Flutter 入門與實戰(六十二):開發一個通用的購物車數量加減元件

島上碼農發表於2021-08-24

這是我參與8月更文挑戰的第25天,活動詳情檢視:8月更文挑戰

前言

本篇繼續購物清單應用的完善,解決完離線儲存後,目前的購物清單存在兩個問題:一是沒法刪除(女朋友的購物車除外,見下圖);二是我們的中介軟體的寫法的 if...else 巢狀會隨著 Action 的增加而增加,可以進一步改進。

image.png

本篇來解決這兩個問題,話不多說,開幹!

購物車數量加減元件

在商城應用中,我們在購物車經常會用到數量控制的加減元件,如下圖所示。點選加號數量加1,點選減號數量減1。

image.png

我們也來實現一個這樣的通用元件:

  • 數量顯示購物清單當前物品的數量;
  • 點選加號數量加1;
  • 點選減號數量減1,如果減到0就把該項刪除。

在開發元件前,我們應該先定義好元件的對外介面(包括屬性和互動方法),在購物車數量加減元件中,有如下介面需要與使用它的元件進行互動:

  • 數量屬性:元件本身不承載業務,因此這個數量是由上層元件來控制的;我們定義為 count
  • 點選加號的響應方法,我們定義為onAdd,該方法攜帶加1後的數量引數。
  • 點選減號的響應方法,我們定義為 onSub,該方法攜帶減1後的數量引數。
  • 中間文字的寬widthheight,非必傳,由元件自身設定預設引數。

由於元件本身沒有自身的狀態,因此定義為 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 中的通用元件,儘可能地將構造方法定義為 const,表示該物件是不可變的,這樣在渲染過程中效率會更高。我們以後單獨針對這個來一篇介紹。加減元件本身的實現比較簡單,這裡不貼程式碼了,需要程式碼的請看這裡:Redux狀態管理相關程式碼

數量增減業務實現

數量增減照樣我們需要使用 Redux 的狀態管理實現,這裡新增兩個 Action

  • AddItemCountAction:數量加1,攜帶當前的購物項 ShoppingItem
  • SubItemCountAction:數量減1,攜帶當前的購物項 ShoppingItem

然後在 Reducer 中處理增減數量邏輯:

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();
  // 刪除數量等於0的元素
  newItems = newItems.where((item) => item.count > 0).toList();

  return newItems;
}
複製程式碼

在中介軟體中同樣需要在加減數量時進行離線儲存處理。

void shoppingListMiddleware(
    Store<ShoppingListState> store, dynamic action, NextDispatcher next) async {
  if (action is ReadOfflineAction) {
    // 從離線儲存中讀取清單
  } 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();
}

複製程式碼

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

螢幕錄製2021-08-24 下午8.46.33.gif

中介軟體程式碼優化

效果是達到預期了,但是中介軟體的程式碼有點 Low,if 裡面套了4個 Action,而且如果 Action 一多,那 if...else 簡直沒法看。對於這種情況,Redux 中提供了一種指定 Action 的響應方式,那就是:

TypedMiddleware<T, Action>(middlewareFunction)
複製程式碼

其中 T 是對應的狀態類,Action 是對應的 Action 類,而 middlewareFunction 就是對應 Action 的處理方法。通過這種方式可以將 Action 和對應的處理中介軟體繫結起來,然後在執行中介軟體的時候會找到對繫結的 Action 對應的中介軟體執行,從而避免了if...else 的情況。然後組成一箇中介軟體陣列,這樣就不需要寫那麼多 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);
}
複製程式碼

改造完的中介軟體程式碼看似變多了,其實是因為我們的幾個 Action的操作類似導致的,如果 Action 的業務差別比較大,程式碼量不會增加多少,但是整個程式碼的維護性增強了很多。當然,建立 Store 的 程式碼的中介軟體引數也需要改一下:

final store = Store<ShoppingListState>(
  shoppingListReducer,
  initialState: ShoppingListState.initial(),
  middleware: shopplingListMiddleware(),
);
複製程式碼

總結

本篇完成了購物數量加減元件的開發,以及使用了TypedMiddleware將中介軟體處理方法與對應的 Action 進行繫結避免過多的 if...else 判斷,增強了中介軟體的可維護性。使用了幾次 Redux 後,我們也會發現 Redux 的架構、業務邏輯和UI介面的職責、界限更為清晰。對於大中型專案來說,在可維護性上可能會比 Provider更勝一籌。

相關文章