阅读 455

Flutter入门与实战(六十一):Redux 之利用中间件完成离线存储购物清单

这是我参与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 入门与实战专栏源码

👍🏻:觉得有收获请点个赞鼓励一下!

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

💬:评论交流,互相进步!

文章分类
Android