phase3: utils.dart null-safe (cart logic + dio5 error handling)

- cart event handlers rewritten with local list + nullable model fields
- getCartInfo*/addCart/removeCart helpers: nullable List<CartInfo>? params
- addSubproductToCard/addSubproductQty/removeSubproduct: null-safe nested access
- showMessageDialog: DioException + promoted response local
- newCartLineItem params nullable; getWeekDayName default return
- launchURL->launch; RuntimeError code nullable
This commit is contained in:
2026-07-24 20:13:07 +08:00
parent 88dd90ed7c
commit 7e8182259f

View File

@@ -37,89 +37,60 @@ typedef void OnOk();
typedef void OnLoadImageError(); typedef void OnLoadImageError();
class Utils { class Utils {
static StreamSubscription onProductWillAddToCartSubscription; static StreamSubscription? onProductWillAddToCartSubscription;
static StreamSubscription onProductWillRemoveFromCartSubscription; static StreamSubscription? onProductWillRemoveFromCartSubscription;
init() async { init() async {
onProductWillAddToCartSubscription = onProductWillAddToCartSubscription =
eventBus.on<OnProductWillAddToCart>().listen((event) { eventBus.on<OnProductWillAddToCart>().listen((event) {
CartInfo cartInfo = CartInfo? found = getCartInfoByBusiness(store.state.cartInfos, event.business);
getCartInfoByBusiness(store.state.cartInfos, event.business); final List<CartLineItem> existing =
(found != null && found.productList != null && found.productList!.isNotEmpty)
if (cartInfo == null || cartInfo.productList.length == 0) { ? found.productList! : <CartLineItem>[];
cartInfo = CartInfo(); CartInfo cartInfo = found ?? CartInfo();
if (existing.isEmpty) {
cartInfo.id = 0; cartInfo.id = 0;
cartInfo.amountPaid = 0.0; cartInfo.amountPaid = 0.0;
cartInfo.businessInfo = event.business; cartInfo.businessInfo = event.business;
cartInfo.extraFeeList = []; cartInfo.extraFeeList = [];
cartInfo.discountList = []; cartInfo.discountList = [];
CartLineItem lineItem = newCartLineItem( CartLineItem lineItem = newCartLineItem(
id: 0, id: 0, price: event.price, product: event.product,
price: event.price, name: event.product.name, description: event.description, quantity: 1.0);
product: event.product,
name: event.product.name,
description: event.description,
quantity: 1.0);
cartInfo.productList = [lineItem]; cartInfo.productList = [lineItem];
addSubproductToCard(cartInfo, lineItem); addSubproductToCard(cartInfo, lineItem);
store.dispatch(new UpdateCartInfo(addCartInfoToCartInfoList( store.dispatch(UpdateCartInfo(addCartInfoToCartInfoList(store.state.cartInfos, cartInfo)));
store.state.cartInfos, cartInfo))); eventBus.fire(OnCartInfoUpdated());
eventBus.fire(new OnCartInfoUpdated());
print('#1');
} else { } else {
if (event.product.productAttributes.length > 0) { if ((event.product.productAttributes?.length ?? 0) > 0) {
CartLineItem lineItem = newCartLineItem( CartLineItem lineItem = newCartLineItem(
id: 0, id: 0, price: event.price, product: event.product,
price: event.price, name: event.product.name, description: event.description, quantity: 1.0);
product: event.product, existing.add(lineItem);
name: event.product.name,
description: event.description,
quantity: 1.0);
cartInfo.productList.add(lineItem);
addSubproductToCard(cartInfo, lineItem); addSubproductToCard(cartInfo, lineItem);
store.dispatch(new UpdateCartInfo(addCartInfoToCartInfoList( store.dispatch(UpdateCartInfo(addCartInfoToCartInfoList(store.state.cartInfos, cartInfo)));
store.state.cartInfos, cartInfo))); eventBus.fire(OnCartInfoUpdated());
print('#2');
eventBus.fire(new OnCartInfoUpdated());
} else { } else {
int found = -1; int foundIndex = -1;
for (var i = 0; i < cartInfo.productList.length; i++) { for (var i = 0; i < existing.length; i++) {
if (event.product.id == cartInfo.productList[i].product.id) { if (existing[i].product?.id == event.product.id) { foundIndex = i; break; }
found = i;
break;
} }
} if (foundIndex == -1) {
if (found == -1) {
CartLineItem lineItem = newCartLineItem( CartLineItem lineItem = newCartLineItem(
id: 0, id: 0, price: event.price, product: event.product,
price: event.price, name: event.product.name, description: event.description, quantity: 1.0);
product: event.product, existing.add(lineItem);
name: event.product.name,
description: event.description,
quantity: 1.0);
cartInfo.productList.add(lineItem);
addSubproductToCard(cartInfo, lineItem); addSubproductToCard(cartInfo, lineItem);
store.dispatch(new UpdateCartInfo(addCartInfoToCartInfoList( store.dispatch(UpdateCartInfo(addCartInfoToCartInfoList(store.state.cartInfos, cartInfo)));
store.state.cartInfos, cartInfo))); eventBus.fire(OnCartInfoUpdated());
print('#3');
eventBus.fire(new OnCartInfoUpdated());
} else { } else {
if (cartInfo.productList[found].quantity + 1.0 > if ((existing[foundIndex].quantity ?? 0) + 1.0 > (event.product.leftNum ?? double.infinity)) {
event.product.leftNum) { // insufficient stock
// Fluttertoast.showToast(
// msg: S.of(context).product_insufficient,
// toastLength: Toast.LENGTH_SHORT,
// gravity: ToastGravity.CENTER,
// backgroundColor: Colors.red,
// textColor: Colors.white);
} else { } else {
cartInfo.productList[found].quantity += 1; existing[foundIndex].quantity = (existing[foundIndex].quantity ?? 0) + 1;
addSubproductQty(cartInfo, cartInfo.productList[found]); addSubproductQty(cartInfo, existing[foundIndex]);
store.dispatch(new UpdateCartInfo( store.dispatch(UpdateCartInfo(addCartInfoToCartInfoList(store.state.cartInfos, cartInfo)));
addCartInfoToCartInfoList( eventBus.fire(OnCartInfoUpdated());
store.state.cartInfos, cartInfo)));
print('#4');
eventBus.fire(new OnCartInfoUpdated());
} }
} }
} }
@@ -127,52 +98,45 @@ class Utils {
}); });
onProductWillRemoveFromCartSubscription = onProductWillRemoveFromCartSubscription =
eventBus.on<OnProductWillRemoveFromCart>().listen((event) { eventBus.on<OnProductWillRemoveFromCart>().listen((event) {
CartInfo cartInfo = CartInfo? found = getCartInfoByBusiness(store.state.cartInfos, event.business);
getCartInfoByBusiness(store.state.cartInfos, event.business); if (found != null && found.productList != null) {
if (cartInfo != null) { final existing = found.productList!;
if (cartInfo.productList.length > 0) { if (existing.isNotEmpty) {
if (event.productListIndex != -1) { if (event.productListIndex != -1) {
if (cartInfo.productList[event.productListIndex].quantity <= 1) { if ((existing[event.productListIndex].quantity ?? 0) <= 1) {
String uuid = cartInfo.productList[event.productListIndex].uuid; String uuid = existing[event.productListIndex].uuid ?? '';
cartInfo.productList.removeAt(event.productListIndex); existing.removeAt(event.productListIndex);
removeSubproduct(cartInfo, uuid); removeSubproduct(found, uuid);
} else { } else {
cartInfo.productList[event.productListIndex].quantity -= 1; existing[event.productListIndex].quantity = (existing[event.productListIndex].quantity ?? 0) - 1;
addSubproductQty(cartInfo, cartInfo.productList[event.productListIndex], remove: true); addSubproductQty(found, existing[event.productListIndex], remove: true);
} }
} else { } else {
int productListIndex = -1; int idx = -1;
for (var i = 0; i < cartInfo.productList.length; i++) { for (var i = 0; i < existing.length; i++) {
if (cartInfo.productList[i].product.id == event.product.id) { if (existing[i].product?.id == event.product.id) { idx = i; break; }
productListIndex = i;
break;
} }
} if (idx != -1) {
if (productListIndex != -1) { if ((existing[idx].quantity ?? 0) <= 1) {
if (cartInfo.productList[productListIndex].quantity <= 1) { String uuid = existing[idx].uuid ?? '';
String uuid = cartInfo.productList[productListIndex].uuid; existing.removeAt(idx);
cartInfo.productList.removeAt(productListIndex); removeSubproduct(found, uuid);
removeSubproduct(cartInfo, uuid);
} else { } else {
cartInfo.productList[productListIndex].quantity -= 1; existing[idx].quantity = (existing[idx].quantity ?? 0) - 1;
addSubproductQty(cartInfo, cartInfo.productList[productListIndex], remove: true); addSubproductQty(found, existing[idx], remove: true);
} }
} }
} }
} }
if (existing.isEmpty) {
if (cartInfo.productList.length <= 0) { store.dispatch(UpdateCartInfo(removeCartInfoFromCartInfoList(store.state.cartInfos, found)));
store.dispatch(UpdateCartInfo(removeCartInfoFromCartInfoList(
store.state.cartInfos, cartInfo)));
} else { } else {
store.dispatch(UpdateCartInfo(addCartInfoToCartInfoList( store.dispatch(UpdateCartInfo(addCartInfoToCartInfoList(store.state.cartInfos, found)));
store.state.cartInfos, cartInfo)));
} }
eventBus.fire(new OnCartInfoUpdated()); eventBus.fire(OnCartInfoUpdated());
} }
}); });
} }
static bool equalsIgnoreCase(String a, String b) => static bool equalsIgnoreCase(String a, String b) =>
(a == null && b == null) || (a == null && b == null) ||
(a != null && b != null && a.toLowerCase() == b.toLowerCase()); (a != null && b != null && a.toLowerCase() == b.toLowerCase());
@@ -211,7 +175,7 @@ class Utils {
return false; return false;
} }
static Map<String, dynamic> stringToJson(String string) { static Map<String, dynamic>? stringToJson(String string) {
if (string == null || string.isEmpty) { if (string == null || string.isEmpty) {
return null; return null;
} }
@@ -332,7 +296,7 @@ class Utils {
); );
} }
static showLoadingDialog(BuildContext context, {String message}) { static showLoadingDialog(BuildContext context, {String? message}) {
showDialog( showDialog(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
@@ -460,21 +424,24 @@ class Utils {
} }
static void showMessageDialog(BuildContext context, dynamic error, static void showMessageDialog(BuildContext context, dynamic error,
{String title, List<Widget> actions, OnOk onOk}) { {String? title, List<Widget>? actions, OnOk? onOk}) {
String message = ''; String message = '';
print('error $error'); print('error $error');
if (error is DioError && error.response != null) { if (error is DioException) {
if (error.response.data['message'] != null) { final resp = error.response;
message = error.response.data['message']; if (resp != null) {
} else if (error.response.data['error'] != null) { if (resp.data['message'] != null) {
if (error.response.data['error'] is String) { message = resp.data['message'];
message = error.response.data['error']; } else if (resp.data['error'] != null) {
} else if (error.response.data['error']['errmsg'] != null) { if (resp.data['error'] is String) {
message = error.response.data['error']['errmsg']; message = resp.data['error'];
} else if (error.response.data['error']['message'] != null) { } else if (resp.data['error']['errmsg'] != null) {
message = error.response.data['error']['message']; message = resp.data['error']['errmsg'];
} else if (resp.data['error']['message'] != null) {
message = resp.data['error']['message'];
} else { } else {
message = error.response.data.toString(); message = resp.data.toString();
}
} }
} }
} else if (error is String) { } else if (error is String) {
@@ -623,26 +590,26 @@ class Utils {
} }
} }
static CartInfo getCartInfoByBusiness( static CartInfo? getCartInfoByBusiness(
List<CartInfo> cartInfos, Business business) { List<CartInfo>? cartInfos, Business business) {
if (cartInfos == null || cartInfos.length <= 0) { if (cartInfos == null || cartInfos.length <= 0) {
return null; return null;
} }
for (var i = 0; i < cartInfos.length; i++) { for (var i = 0; i < cartInfos.length; i++) {
if (cartInfos[i].businessInfo.id == business.id) { if (cartInfos[i].businessInfo?.id == business.id) {
return cartInfos[i]; return cartInfos[i];
} }
} }
return null; return null;
} }
static CartInfo getCartInfoByBusinessId( static CartInfo? getCartInfoByBusinessId(
List<CartInfo> cartInfos, int businessId) { List<CartInfo>? cartInfos, int businessId) {
if (cartInfos == null || cartInfos.length <= 0) { if (cartInfos == null || cartInfos.length <= 0) {
return null; return null;
} }
for (var i = 0; i < cartInfos.length; i++) { for (var i = 0; i < cartInfos.length; i++) {
if (cartInfos[i].businessInfo.id == businessId) { if (cartInfos[i].businessInfo?.id == businessId) {
return cartInfos[i]; return cartInfos[i];
} }
} }
@@ -650,13 +617,13 @@ class Utils {
} }
static List<CartInfo> addCartInfoToCartInfoList( static List<CartInfo> addCartInfoToCartInfoList(
List<CartInfo> cartInfos, CartInfo cartInfo) { List<CartInfo>? cartInfos, CartInfo cartInfo) {
if (cartInfos == null || cartInfos.length <= 0) { if (cartInfos == null || cartInfos.length <= 0) {
cartInfos = [cartInfo]; cartInfos = [cartInfo];
} else { } else {
bool found = false; bool found = false;
for (var i = 0; i < cartInfos.length; i++) { for (var i = 0; i < cartInfos.length; i++) {
if (cartInfos[i].businessInfo.id == cartInfo.businessInfo.id) { if (cartInfos[i].businessInfo?.id == cartInfo.businessInfo?.id) {
cartInfos[i] = cartInfo; cartInfos[i] = cartInfo;
found = true; found = true;
break; break;
@@ -675,13 +642,13 @@ class Utils {
} }
static List<CartInfo> removeCartInfoFromCartInfoList( static List<CartInfo> removeCartInfoFromCartInfoList(
List<CartInfo> cartInfos, CartInfo cartInfo) { List<CartInfo>? cartInfos, CartInfo? cartInfo) {
if (cartInfos == null || cartInfos.length <= 0 || cartInfo == null) { if (cartInfos == null || cartInfos.length <= 0 || cartInfo == null) {
return []; return [];
} }
int removeIndex = -1; int removeIndex = -1;
for (var i = 0; i < cartInfos.length; i++) { for (var i = 0; i < cartInfos.length; i++) {
if (cartInfo.businessInfo.id == cartInfos[i].businessInfo.id) { if (cartInfo.businessInfo?.id == cartInfos[i].businessInfo?.id) {
removeIndex = i; removeIndex = i;
break; break;
} }
@@ -697,9 +664,9 @@ class Utils {
return cartInfos; return cartInfos;
} }
static void clearCart(BuildContext context, CartInfo cartInfo, static void clearCart(BuildContext context, CartInfo? cartInfo,
{OnComplete onComplete}) { {OnComplete? onComplete}) {
if (cartInfo != null && cartInfo.productList.length > 0) { if (cartInfo != null && cartInfo.productList != null && cartInfo.productList!.length > 0) {
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
@@ -733,7 +700,7 @@ class Utils {
} }
static void requireLogin(BuildContext context, static void requireLogin(BuildContext context,
{String returnUrl, bool replace = true, int delayMilliseconds = 400}) { {String? returnUrl, bool replace = true, int delayMilliseconds = 400}) {
Future.delayed(Duration(milliseconds: delayMilliseconds), () { Future.delayed(Duration(milliseconds: delayMilliseconds), () {
if (returnUrl != null) { if (returnUrl != null) {
store.dispatch(UpdateRedirectRoute(returnUrl)); store.dispatch(UpdateRedirectRoute(returnUrl));
@@ -744,7 +711,7 @@ class Utils {
static String getFirstNumberFromString(String numString) { static String getFirstNumberFromString(String numString) {
final intInStr = RegExp(r'\d+'); final intInStr = RegExp(r'\d+');
return intInStr.firstMatch(numString).group(0); return intInStr.firstMatch(numString)?.group(0) ?? '';
} }
static void loadProducts(int businessId, int categoryId, int page, bool more, static void loadProducts(int businessId, int categoryId, int page, bool more,
@@ -864,15 +831,18 @@ class Utils {
return S.of(context).sunday; return S.of(context).sunday;
} }
break; break;
default:
return '';
} }
return '';
} }
static int getProductLineInOrder(CartInfo cartInfo) { static int getProductLineInOrder(CartInfo cartInfo) {
int qty = 0; int qty = 0;
for (CartLineItem lineItem in cartInfo.productList) { for (CartLineItem lineItem in (cartInfo.productList ?? <CartLineItem>[])) {
if (lineItem.product.id != null) { if (lineItem.product?.id != null) {
qty += 1; qty += 1;
} }
} }
@@ -880,10 +850,10 @@ class Utils {
} }
static void orderAgain(BuildContext context, CartInfo cartInfo) { static void orderAgain(BuildContext context, CartInfo cartInfo) {
cartInfo.productList.removeWhere((element) => element.product == null || element.product.id == null); cartInfo.productList?.removeWhere((element) => element.product == null || element.product?.id == null);
store.dispatch(UpdateCartInfo(Utils.addCartInfoToCartInfoList(store.state.cartInfos, cartInfo))); store.dispatch(UpdateCartInfo(Utils.addCartInfoToCartInfoList(store.state.cartInfos, cartInfo)));
eventBus.fire(new OnCartInfoUpdated()); eventBus.fire(new OnCartInfoUpdated());
Routes.router.navigateTo(context, '/checkout/${cartInfo.businessInfo.id}'); Routes.router.navigateTo(context, '/checkout/${cartInfo.businessInfo?.id}');
} }
static String getOrderStatus(BuildContext context, int statusCode) { static String getOrderStatus(BuildContext context, int statusCode) {
@@ -904,18 +874,18 @@ class Utils {
} }
static CartLineItem newCartLineItem( static CartLineItem newCartLineItem(
{int id, {int? id,
double price, double? price,
Product product, Product? product,
String name, String? name,
String description, String? description,
double quantity, double? quantity,
String parentUuid, String? parentUuid,
}) { }) {
CartLineItem lineItem = CartLineItem(); CartLineItem lineItem = CartLineItem();
lineItem.unitPrice = price; lineItem.unitPrice = price;
lineItem.product = product; lineItem.product = product;
lineItem.name = product.name; lineItem.name = product?.name;
lineItem.description = description; lineItem.description = description;
lineItem.quantity = quantity; lineItem.quantity = quantity;
lineItem.parentUuid = parentUuid; lineItem.parentUuid = parentUuid;
@@ -923,43 +893,45 @@ class Utils {
} }
static void addSubproductToCard(CartInfo cartInfo, CartLineItem lineItem) { static void addSubproductToCard(CartInfo cartInfo, CartLineItem lineItem) {
if (lineItem.product.subproducts.length > 0) { final product = lineItem.product;
for (int j = 0; j < lineItem.product.subproducts.length; j++) { final subs = product?.subproducts;
cartInfo.productList.add( if (subs == null || subs.isEmpty) return;
newCartLineItem( for (int j = 0; j < subs.length; j++) {
final sp = subs[j].product;
cartInfo.productList?.add(newCartLineItem(
id: 0, id: 0,
price: 0.0, price: 0.0,
product: Product( product: Product(
lineItem.product.subproducts[j].product.id, sp?.id ?? 0,
lineItem.product.businessId, product?.businessId ?? 0,
lineItem.product.subproducts[j].product.name, sp?.name ?? '',
lineItem.product.subproducts[j].product.price, sp?.price ?? 0.0,
lineItem.product.subproducts[j].product.description, sp?.description ?? '',
lineItem.product.subproducts[j].product.image, sp?.image ?? '',
lineItem.product.subproducts[j].product.productAttributes sp?.productAttributes ?? []
), ),
name: lineItem.product.subproducts[j].product.name, name: sp?.name ?? '',
description: lineItem.product.subproducts[j].product.description, description: sp?.description ?? '',
quantity: 1.0, quantity: 1.0,
parentUuid: lineItem.uuid parentUuid: lineItem.uuid
) ));
);
}
} }
} }
static void addSubproductQty(CartInfo cartInfo, CartLineItem lineItem, static void addSubproductQty(CartInfo cartInfo, CartLineItem lineItem,
{bool remove = false}) { {bool remove = false}) {
if (lineItem.product.subproducts.length > 0) { final product = lineItem.product;
for (var i = 0; i < cartInfo.productList.length; i++) { final subs = product?.subproducts;
CartLineItem lineItem1 = cartInfo.productList[i]; if (subs == null || subs.isEmpty) return;
for (var i = 0; i < (cartInfo.productList?.length ?? 0); i++) {
CartLineItem lineItem1 = cartInfo.productList![i];
if (lineItem1.parentUuid == lineItem.uuid) { if (lineItem1.parentUuid == lineItem.uuid) {
for (var j = 0; j < lineItem.product.subproducts.length; j++) { for (var j = 0; j < subs.length; j++) {
if (lineItem.product.subproducts[j].product.id == lineItem1.product.id) { if (subs[j].product?.id == lineItem1.product?.id) {
if (remove) { if (remove) {
lineItem1.quantity -= lineItem.product.subproducts[j].quantity; lineItem1.quantity = (lineItem1.quantity ?? 0) - (subs[j].quantity ?? 0);
} else { } else {
lineItem1.quantity += lineItem.product.subproducts[j].quantity; lineItem1.quantity = (lineItem1.quantity ?? 0) + (subs[j].quantity ?? 0);
} }
break; break;
} }
@@ -967,10 +939,9 @@ class Utils {
} }
} }
} }
}
static void removeSubproduct(CartInfo cartInfo, String uuid) { static void removeSubproduct(CartInfo cartInfo, String uuid) {
cartInfo.productList.removeWhere((element) => element.parentUuid == uuid); cartInfo.productList?.removeWhere((element) => element.parentUuid == uuid);
} }
static void openEmail(String email) async { static void openEmail(String email) async {
@@ -980,7 +951,7 @@ class Utils {
); );
String url = params.toString(); String url = params.toString();
if (await canLaunch(url)) { if (await canLaunch(url)) {
await launchURL(url); await launch(url);
} else { } else {
print('Could not launch $url'); print('Could not launch $url');
} }
@@ -989,13 +960,13 @@ class Utils {
static void callPhone(String phone) async { static void callPhone(String phone) async {
String callNo = 'tel://$phone'; String callNo = 'tel://$phone';
if (await canLaunch(callNo)) { if (await canLaunch(callNo)) {
await launchURL(callNo); await launch(callNo);
} else { } else {
print('Could not call $phone'); print('Could not call $phone');
} }
} }
static Widget buildLine(String name, dynamic value, {double nameSize, double valueSize}) { static Widget buildLine(String name, dynamic value, {double? nameSize, double? valueSize}) {
Row row = Row( Row row = Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -1045,7 +1016,7 @@ class Utils {
} }
class RuntimeError extends Error { class RuntimeError extends Error {
final int code; final int? code;
final String message; final String message;
RuntimeError(this.message, {this.code}); RuntimeError(this.message, {this.code});
String toString() => "Runtime Error: $message"; String toString() => "Runtime Error: $message";