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