Flutter入門與實戰(六十一):Redux 之利用中介軟體完成離線儲存購物清單

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

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

前言

上一篇我們完成了購物清單的基本功能,但是存在幾個問題:

  • 可以新增重複的購物項,這樣會導致 CheckBox 選擇的時候出現兩個重複選項同時被操作的問題;
  • 沒有離線儲存,如果是真的購物清單,一退出 App 資料就丟失,那這個應用根本沒法用;
  • 無法刪除購物項。

本篇我們就來解決這些問題。

重複新增購物項的處理

重複新增的時候,我們處理為對於重複新增項,在原有購物項基礎上加1,並且在清單顯示購物項數量,這樣就可以很好地處理這個問題了。重複新增的處理相對簡單,一是在 ShoppingItem 中增加一個數量 count 屬性,二是我們在 Reducer 中響應AddItemAction 的時候,檢查到有重複的項時,把該項的數量加1即可。

這裡我們抽出兩個通用的方法addItemActionHandlertoggleItemStateActionHandler,以便其他地方也可以呼叫。

List<ShoppingItem> addItemActionHandler(
    List<ShoppingItem> oldItems, ShoppingItem newItem) {
  List<ShoppingItem> newItems = [];

  if (oldItems.length > 0) {
    bool duplicated = false;
    newItems = oldItems.map((item) {
      if (item == newItem) {
        duplicated = true;
        return ShoppingItem(
            name: item.name, selected: item.selected, count: item.count + 1);
      }
      return item;
    }).toList();
    if (!duplicated) {
      newItems.add(newItem);
    }
  } else {
    newItems.add(newItem);
  }

  return newItems;
}

List<ShoppingItem> toggleItemStateActionHandler(
    List<ShoppingItem> oldItems, ShoppingItem newItem) {
  List<ShoppingItem> newItems = oldItems.map((item) {
    if (item == newItem)
      return ShoppingItem(
          name: item.name, selected: !item.selected, count: item.count);
    return item;
  }).toList();

  return newItems;
}
複製程式碼

離線儲存

離線儲存我們使用 shared_preferences 外掛來儲存離線購物清單,這個外掛我們在手寫一個持久化的 CookieManager的時候就有介紹過了。shared_preferences 只能儲存 boolintdoubleStringList<String>等基本型別,這裡我們統一將清單列表轉換為 json 字串儲存。

class ShoppingItem {
  final String name;
  final bool selected;
  final int count;

  ShoppingItem({required this.name, this.selected = false, this.count = 1});

  bool operator ==(Object? other) {
    if (other == null || !(other is ShoppingItem)) return false;
    return other.name == this.name;
  }

  @override
  get hashCode => name.hashCode;

  Map<String, String> toJson() {
    return {
      'name': name,
      'selected': selected.toString(),
      'count': count.toString(),
    };
  }

  factory ShoppingItem.fromJson(Map<String, dynamic> json) {
    return ShoppingItem(
      name: json['name']!,
      selected: json['selected'] == 'true',
      count: int.parse(json['count']!),
    );
  }
}

複製程式碼

由於離線儲存是非同步操作,因此需要使用中介軟體完成非同步儲存操作。當新增購物項或改變購物項狀態時,將最新的清單進行離線儲存。

// 中介軟體
const SHOPPLINT_LIST_KEY = 'shoppingList';
void shoppingListMiddleware(
    Store<ShoppingListState> store, dynamic action, NextDispatcher next) async {
	//...
  if (action is AddItemAction || action is ToggleItemStateAction) {
    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 得到需要儲存的 List
List<Map<String, String>> _prepareForSave(
    List<ShoppingItem> oldItems, dynamic action) {
  List<ShoppingItem> newItems = [];
  if (action is AddItemAction) {
    newItems = addItemActionHandler(oldItems, action.item);
  }
  if (action is ToggleItemStateAction) {
    newItems = toggleItemStateActionHandler(oldItems, action.item);
  }

  return newItems.map((item) => item.toJson()).toList();
}
複製程式碼

從離線資料中恢復清單

離線儲存搞定了,接下來的問題是如何從離線資料中恢復清單。這個恢復要在 App啟動的時候做。也就是啟動後,需要從離線儲存中讀取購物清單填充到狀態中。同樣的,我們這裡需要2個 Action

  • ReadOfflineAction:從離線快取讀取資料,離線讀取是非同步操作,因此也需要在中介軟體完成。完成後排程ReadOfflineSuccessAction
  • ReadOfflineSuccessAction:讀取成功,攜帶離線資料更新狀態資料。

有了這兩個操作後,中介軟體的程式碼變成:

void shoppingListMiddleware(
    Store<ShoppingListState> store, dynamic action, NextDispatcher next) async {
  if (action is ReadOfflineAction) {
    SharedPreferences.getInstance().then((prefs) {
      dynamic offlineList = prefs.get(SHOPPLINT_LIST_KEY'shoppingList');
      if (offlineList != null && offlineList is String) {
        store.dispatch(
            ReadOfflineSuccessAction(offlineList: json.decode(offlineList)));
      }
    });
  } else if (action is AddItemAction || action is ToggleItemStateAction) {
    List<Map<String, String>> listToSave =
        _prepareForSave(store.state.shoppingItems, action);
    SharedPreferences.getInstance().then(
        (prefs) => prefs.setString(SHOPPLINT_LIST_KEY, json.encode(listToSave)));
  } else {
    // ReadOfflineSuccessAction:無操作
  }

  next(action);
}
複製程式碼

這裡說一下自己除錯時候踩的一個坑,當時中介軟體的程式碼寫成了:

if (action is ReadOfflineAction) {
} else {
  // 離線儲存資料
}
複製程式碼

結果每隔一次啟動,資料就丟失了,百思不得其解!然後在離線儲存那段程式碼打了一個斷點,才發現是因為 ReadOfflineSuccessAction 的時候跳轉到這裡面去了,結果 store.state.shoppingItems 因為還沒更新到,是空陣列,直接存了空陣列了?。 接下來還剩一個問題,如何在啟動 App 的時候排程 ReadOfflineAction 呢?這個時候 StoreBuilder 就能夠排上用場了。StoreBuilder 提供了狀態的生命週期函式的回撥設定,可以通過StoreBuilder構建下級狀態依賴元件,然後指定對應的生命週期回撥方法:

const StoreBuilder({
  Key? key,
  required this.builder,
  this.onInit,
  this.onDispose,
  this.rebuildOnChange = true,
  this.onWillChange,
  this.onDidChange,
  this.onInitialBuild,
}) : super(key: key);
複製程式碼

在這裡,我們指定 onInit 初始化回撥方法即可,在 onInit 中排程 ReadOfflineAction 就能夠達到我們啟動後讀取離線資料的目的。

home: StoreBuilder<ShoppingListState>(
  onInit: (store) => store.dispatch(ReadOfflineAction()),
  builder: (context, store) => ShoppingListHome(),
),
複製程式碼

就這樣,搞定!

執行效果

執行效果如下所示,現在不用擔心搞丟購物清單了!原始碼已上傳至:Redux 狀態管理原始碼

購物清單離線儲存效果.gif

總結

本篇介紹了使用 StoreBuilder 引入狀態生命週期勾子函式,並在初始化階段讀取離線資料。然後使用 Redux 的中介軟體完成了資料的儲存和離線資料的載入,從而完成了一個支援離線儲存的購物清單。這裡還存在一個問題,那就是沒法減少或刪除購物項,這不科學啊,這又不是女朋友的購物車,必須可以反悔才行!下一篇,我們來一個通用的購物數量增減元件。


我是島上碼農,微信公眾號同名,這是Flutter 入門與實戰的專欄文章,對應原始碼請看這裡:Flutter 入門與實戰專欄原始碼

??:覺得有收穫請點個贊鼓勵一下!

?:收藏文章,方便回看哦!

?:評論交流,互相進步!

相關文章