前言
上一篇我们完成了购物清单的基本功能,但是存在几个问题:
本篇我们就来解决这些问题。
重复添加购物项的处理
重复添加的时候,我们处理为对于重复添加项,在原有购物项基础上加 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 插件来存储离线购物清单,这个插件我们之前就有介绍过了。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
:
有了这两个操作后,中间件的代码变成:
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
的中间件完成了数据的存储和离线数据的加载,从而完成了一个支持离线存储的购物清单。这里还存在一个问题,那就是没法减少或删除购物项,这不科学啊,这又不是女朋友的购物车,必须可以反悔才行!下一篇,我们来一个通用的购物数量增减组件。
评论