這是我參與8月更文挑戰的第24天,活動詳情檢視:8月更文挑戰
前言
上一篇我們完成了購物清單的基本功能,但是存在幾個問題:
- 可以新增重複的購物項,這樣會導致 CheckBox 選擇的時候出現兩個重複選項同時被操作的問題;
- 沒有離線儲存,如果是真的購物清單,一退出 App 資料就丟失,那這個應用根本沒法用;
- 無法刪除購物項。
本篇我們就來解決這些問題。
重複新增購物項的處理
重複新增的時候,我們處理為對於重複新增項,在原有購物項基礎上加1,並且在清單顯示購物項數量,這樣就可以很好地處理這個問題了。重複新增的處理相對簡單,一是在 ShoppingItem
中增加一個數量 count
屬性,二是我們在 Reducer
中響應AddItemAction
的時候,檢查到有重複的項時,把該項的數量加1即可。
這裡我們抽出兩個通用的方法addItemActionHandler
和toggleItemStateActionHandler
,以便其他地方也可以呼叫。
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
只能儲存 bool
,int
,double
,String
和 List<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 狀態管理原始碼。
總結
本篇介紹了使用 StoreBuilder
引入狀態生命週期勾子函式,並在初始化階段讀取離線資料。然後使用 Redux
的中介軟體完成了資料的儲存和離線資料的載入,從而完成了一個支援離線儲存的購物清單。這裡還存在一個問題,那就是沒法減少或刪除購物項,這不科學啊,這又不是女朋友的購物車,必須可以反悔才行!下一篇,我們來一個通用的購物數量增減元件。
我是島上碼農,微信公眾號同名,這是Flutter 入門與實戰的專欄文章,對應原始碼請看這裡:Flutter 入門與實戰專欄原始碼。
??:覺得有收穫請點個贊鼓勵一下!
?:收藏文章,方便回看哦!
?:評論交流,互相進步!