Flutter 開發(fā)一個(gè)通用的購物車數(shù)量編輯組件

前言

本篇繼續(xù)購物清單應(yīng)用的完善,解決完離線存儲(chǔ)后,目前的購物清單存在兩個(gè)問題:一是沒法刪除(女朋友的購物車除外,見下圖);二是我們的中間件的寫法的 if...else 嵌套會(huì)隨著 Action 的增加而增加,可以進(jìn)一步改進(jìn)。

image.png

本篇來解決這兩個(gè)問題,話不多說,開干!

購物車數(shù)量加減組件

在商城應(yīng)用中,我們在購物車經(jīng)常會(huì)用到數(shù)量控制的加減組件,如下圖所示。點(diǎn)擊加號(hào)數(shù)量加1,點(diǎn)擊減號(hào)數(shù)量減1。

image.png

我們也來實(shí)現(xiàn)一個(gè)這樣的通用組件:

  • 數(shù)量顯示購物清單當(dāng)前物品的數(shù)量;
  • 點(diǎn)擊加號(hào)數(shù)量加1;
  • 點(diǎn)擊減號(hào)數(shù)量減1,如果減到0就把該項(xiàng)刪除。

在開發(fā)組件前,我們應(yīng)該先定義好組件的對外接口(包括屬性和交互方法),在購物車數(shù)量加減組件中,有如下接口需要與使用它的組件進(jìn)行交互:

  • 數(shù)量屬性:組件本身不承載業(yè)務(wù),因此這個(gè)數(shù)量是由上層組件來控制的;我們定義為 count。
  • 點(diǎn)擊加號(hào)的響應(yīng)方法,我們定義為onAdd,該方法攜帶加1后的數(shù)量參數(shù)。
  • 點(diǎn)擊減號(hào)的響應(yīng)方法,我們定義為 onSub,該方法攜帶減1后的數(shù)量參數(shù)。
  • 中間文本的寬widthheight,非必傳,由組件自身設(shè)定默認(rèn)參數(shù)。

由于組件本身沒有自身的狀態(tài),因此定義為 StatelessWidget,如下所示:


class CartNumber extends StatelessWidget {
  final ValueChanged<int> onSub;
  final ValueChanged<int> onAdd;
  final int count;
  final double width;
  final double height;
  const CartNumber({
    Key? key,
    required this.count,
    required this.onAdd,
    required this.onSub,
    this.width = 40,
    this.height = 40,
  }) : super(key: key);

這里順帶講一下,對于Flutter 中的通用組件,盡可能地將構(gòu)造方法定義為 const,表示該對象是不可變的,這樣在渲染過程中效率會(huì)更高。我們以后單獨(dú)針對這個(gè)來一篇介紹。加減組件本身的實(shí)現(xiàn)比較簡單,這里不貼代碼了,需要代碼的請看這里:Redux狀態(tài)管理相關(guān)代碼。

數(shù)量增減業(yè)務(wù)實(shí)現(xiàn)

數(shù)量增減照樣我們需要使用 Redux 的狀態(tài)管理實(shí)現(xiàn),這里新增兩個(gè) Action

  • AddItemCountAction:數(shù)量加1,攜帶當(dāng)前的購物項(xiàng) ShoppingItem。
  • SubItemCountAction:數(shù)量減1,攜帶當(dāng)前的購物項(xiàng) ShoppingItem。

然后在 Reducer 中處理增減數(shù)量邏輯:

ShoppingListState shoppingListReducer(ShoppingListState state, action) {
  // ...
  if (action is AddItemCountAction) {
    var newItems = addItemCountActionHandler(state.shoppingItems, action.item);

    return ShoppingListState(shoppingItems: newItems);
  }
  if (action is SubItemCountAction) {
    var newItems = subItemCountActionHandler(state.shoppingItems, action.item);

    return ShoppingListState(shoppingItems: newItems);
  }

  return state;
}

List<ShoppingItem> addItemCountActionHandler(
    List<ShoppingItem> oldItems, ShoppingItem itemToHandle) {
  List<ShoppingItem> newItems = oldItems.map((item) {
    if (item == itemToHandle) {
      return ShoppingItem(
          name: item.name, selected: item.selected, count: item.count + 1);
    } else {
      return item;
    }
  }).toList();

  return newItems;
}

List<ShoppingItem> subItemCountActionHandler(
    List<ShoppingItem> oldItems, ShoppingItem itemToHandle) {
  List<ShoppingItem> newItems = oldItems.map((item) {
    if (item == itemToHandle) {
      return ShoppingItem(
          name: item.name, selected: item.selected, count: item.count - 1);
    } else {
      return item;
    }
  }).toList();
  // 刪除數(shù)量等于0的元素
  newItems = newItems.where((item) => item.count > 0).toList();

  return newItems;
}

在中間件中同樣需要在加減數(shù)量時(shí)進(jìn)行離線存儲(chǔ)處理。

void shoppingListMiddleware(
    Store<ShoppingListState> store, dynamic action, NextDispatcher next) async {
  if (action is ReadOfflineAction) {
    // 從離線存儲(chǔ)中讀取清單
  } else if (action is AddItemAction ||
      action is ToggleItemStateAction ||
      action is AddItemCountAction ||
      action is SubItemCountAction) {
    List<Map<String, String>> listToSave =
        _prepareForSave(store.state.shoppingItems, action);
    SharedPreferences.getInstance().then((prefs) =>
        prefs.setString(SHOPPLINT_LIST_KEY, json.encode(listToSave)));
  } else if (action is AddItemCountAction) {
  } else {
    // ReadOfflineSuccessAction:無操作
  }

  next(action);
}

List<Map<String, String>> _prepareForSave(
    List<ShoppingItem> oldItems, dynamic action) {
  List<ShoppingItem> newItems = [];
  // ...
  if (action is AddItemCountAction) {
    newItems = addItemCountActionHandler(oldItems, action.item);
  }
  if (action is SubItemCountAction) {
    newItems = subItemCountActionHandler(oldItems, action.item);
  }

  return newItems.map((item) => item.toJson()).toList();
}

就這樣,我們的加減數(shù)量的邏輯就完成了,來看看效果吧。

屏幕錄制2021-08-24 下午8.46.33.gif

中間件代碼優(yōu)化

效果是達(dá)到預(yù)期了,但是中間件的代碼有點(diǎn) Low,if 里面套了4個(gè) Action,而且如果 Action 一多,那 if...else 簡直沒法看。對于這種情況,Redux 中提供了一種指定 Action 的響應(yīng)方式,那就是:

TypedMiddleware<T, Action>(middlewareFunction)

其中 T 是對應(yīng)的狀態(tài)類,Action 是對應(yīng)的 Action 類,而 middlewareFunction 就是對應(yīng) Action 的處理方法。通過這種方式可以將 Action 和對應(yīng)的處理中間件綁定起來,然后在執(zhí)行中間件的時(shí)候會(huì)找到對綁定的 Action 對應(yīng)的中間件執(zhí)行,從而避免了if...else 的情況。然后組成一個(gè)中間件數(shù)組,這樣就不需要寫那么多 if...else 了。改造完的中間件代碼如下:

List<Middleware<ShoppingListState>> shopplingListMiddleware() => [
      TypedMiddleware<ShoppingListState, ReadOfflineAction>(
          _readOfflineActionMiddleware),
      TypedMiddleware<ShoppingListState, AddItemAction>(
          _addItemActionMiddleware),
      TypedMiddleware<ShoppingListState, ToggleItemStateAction>(
          _toggleItemActionMiddleware),
      TypedMiddleware<ShoppingListState, AddItemCountAction>(
          _addItemCountActionMiddleware),
      TypedMiddleware<ShoppingListState, SubItemCountAction>(
          _subItemCountActionMiddleware),
    ];

void _readOfflineActionMiddleware(Store<ShoppingListState> store,
    ReadOfflineAction action, NextDispatcher next) {
  SharedPreferences.getInstance().then((prefs) {
    dynamic offlineList = prefs.get(SHOPPLINT_LIST_KEY);
    if (offlineList != null && offlineList is String) {
      store.dispatch(
          ReadOfflineSuccessAction(offlineList: json.decode(offlineList)));
    }
  });
  next(action);
}

void _addItemActionMiddleware(
    Store<ShoppingListState> store, AddItemAction action, NextDispatcher next) {
  List<Map<String, String>> listToSave =
      _prepareForSave(store.state.shoppingItems, action);
  SharedPreferences.getInstance().then(
      (prefs) => prefs.setString(SHOPPLINT_LIST_KEY, json.encode(listToSave)));
  next(action);
}

void _toggleItemActionMiddleware(Store<ShoppingListState> store,
    ToggleItemStateAction action, NextDispatcher next) {
  List<Map<String, String>> listToSave =
      _prepareForSave(store.state.shoppingItems, action);
  SharedPreferences.getInstance().then(
      (prefs) => prefs.setString(SHOPPLINT_LIST_KEY, json.encode(listToSave)));
  next(action);
}

void _addItemCountActionMiddleware(Store<ShoppingListState> store,
    AddItemCountAction action, NextDispatcher next) {
  List<Map<String, String>> listToSave =
      _prepareForSave(store.state.shoppingItems, action);
  SharedPreferences.getInstance().then(
      (prefs) => prefs.setString(SHOPPLINT_LIST_KEY, json.encode(listToSave)));
  next(action);
}

void _subItemCountActionMiddleware(Store<ShoppingListState> store,
    SubItemCountAction action, NextDispatcher next) {
  List<Map<String, String>> listToSave =
      _prepareForSave(store.state.shoppingItems, action);
  SharedPreferences.getInstance().then(
      (prefs) => prefs.setString(SHOPPLINT_LIST_KEY, json.encode(listToSave)));
  next(action);
}

改造完的中間件代碼看似變多了,其實(shí)是因?yàn)槲覀兊膸讉€(gè) Action的操作類似導(dǎo)致的,如果 Action 的業(yè)務(wù)差別比較大,代碼量不會(huì)增加多少,但是整個(gè)代碼的維護(hù)性增強(qiáng)了很多。當(dāng)然,創(chuàng)建 Store 的 代碼的中間件參數(shù)也需要改一下:

final store = Store<ShoppingListState>(
  shoppingListReducer,
  initialState: ShoppingListState.initial(),
  middleware: shopplingListMiddleware(),
);

總結(jié)

本篇完成了購物數(shù)量加減組件的開發(fā),以及使用了TypedMiddleware將中間件處理方法與對應(yīng)的 Action 進(jìn)行綁定避免過多的 if...else判斷,增強(qiáng)了中間件的可維護(hù)性。使用了幾次 Redux 后,我們也會(huì)發(fā)現(xiàn) Redux 的架構(gòu)、業(yè)務(wù)邏輯和UI界面的職責(zé)、界限更為清晰。對于大中型項(xiàng)目來說,在可維護(hù)性上可能會(huì)比 Provider更勝一籌。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容