写点什么

Flutter 利用 Redux 中间件完成购物清单离线存储

作者:岛上码农
  • 2022 年 6 月 06 日
  • 本文字数:3139 字

    阅读完需:约 10 分钟

Flutter 利用 Redux 中间件完成购物清单离线存储

前言

上一篇我们完成了购物清单的基本功能,但是存在几个问题:


  • 可以添加重复的购物项,这样会导致 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 插件来存储离线购物清单,这个插件我们之前就有介绍过了。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 得到需要存储的 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


  • 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 的中间件完成了数据的存储和离线数据的加载,从而完成了一个支持离线存储的购物清单。这里还存在一个问题,那就是没法减少或删除购物项,这不科学啊,这又不是女朋友的购物车,必须可以反悔才行!下一篇,我们来一个通用的购物数量增减组件。


发布于: 刚刚阅读数: 4
用户头像

岛上码农

关注

用代码连接孤岛,公众号@岛上码农 2022.03.03 加入

从南漂到北,从北漂到南的业余码农

评论

发布
暂无评论
Flutter 利用 Redux 中间件完成购物清单离线存储_flutter_岛上码农_InfoQ写作社区