Flutter示例系列(二)之状态管理-贰(fish-redux)

开发环境:
Mac OS 10.14.5
VSCode 1.36.1
Flutter 1.9.1+hotfix.2

前言

这是第二篇关于状态管理的文章。第一篇见 Flutter示例系列(二)之状态管理-壹(scoped_model)。

写过前端React或者RN的大概都接触过Redux,本文主要讲述阿里团队开发的 fish-redux 框架。还有一个使用较多的框架 flutter-redux,想要了解的可以参考此博文。

概念

1. fish-redux 的前世今生?

fish-redux 是一个基于 Redux 数据管理的组装式 flutter 应用框架, 它特别适用于构建中大型的复杂应用。

它的特点是配置式组装。 一方面我们将一个大的页面,对视图和数据层层拆解为互相独立的 Component|Adapter,上层负责组装,下层负责实现; 另一方面将 Component|Adapter 拆分为 View,Reducer,Effect 等相互独立的上下文无关函数。

所以它会非常干净,易维护,易协作。

fish-redux 的灵感主要来自于 Redux, Elm, Dva 这样的优秀框架。而 fish-redux 站在巨人的肩膀上,将集中,分治,复用,隔离做的更进一步。

官方文档声明,redux适用与中大型项目,所以如果觉得小项目不需要redux管理数据,完全可以不使用,之前没了解过的话,学习起来是有些难度的,用起来较为复杂。

2. 那么fish-redux 和 redux 有什么不同呢?

1)它们是解决不同层面问题的两个框架
Redux 是一个专注于状态管理的框架;fish-redux 是基于 Redux 做状态管理的应用框架。
应用框架不仅仅要解决状态管理的问题,还要解决分治,通信,数据驱动,解耦等等问题。

2)fish-redux 解决了集中和分治的矛盾。
Redux 通过使用者手动组织代码的形式来完成从小的 Reducer 到主 Reducer 的合并过程;
fish-redux 通过显式的表达组件之间的依赖关系,由框架自动完成从细力度的 Reducer 到主 Reducer 的合并过程;
Flutter示例系列(二)之状态管理-贰(fish-redux)_第1张图片

3)fish-redux 提供了一个简单的组件抽象模型
它通过简单的 3 个函数组合而成
Flutter示例系列(二)之状态管理-贰(fish-redux)_第2张图片

4)fish-redux 提供了一个 Adapter 的抽象组件模型
在基础的组件模型以外,fish-redux 提供了一个 Adapter 抽象模型,用来解决在 ListView 上大 Cell 的性能问题。
通过上层抽象,我们得到了逻辑上的 ScrollView,性能上的 ListView。

Flutter示例系列(二)之状态管理-贰(fish-redux)_第3张图片

3. fish-redux提供哪些重要类?

1)Action

  • Action 包含两个字段
    • type
    • payload
  • 推荐的写法是
    • 为一个组件|适配器创建一个 action.dart 文件,包含两个类
      • 为 type 字段起一个枚举类
      • 为 Action 的创建起一个 ActionCreator 类,这样利于约束 payload 的类型。
    • Effect 接受处理的 Action,以 on{Verb} 命名
    • Reducer 接受处理的 Action,以{verb} 命名
    • 示例代码
enum MessageAction {
    onShare,
    shared,
}

class MessageActionCreator {
    static Action onShare(Map payload) {
        return Action(MessageAction.onShare, payload: payload);
    }

    static Action shared() {
        return const Action(MessageAction.shared);
    }
}

2)Reducer

  • Reducer 是一个上下文无关的 pure function。它接收下面的参数
    • T state
    • Action action
  • 它主要包含三方面的信息
    • 接收一个“意图”, 做出数据修改。
    • 如果要修改数据,需要创建一份新的拷贝,修改在拷贝上。
    • 如果数据修改了,它会自动触发 State 的层层数据的拷贝,再以扁平化方式通知组件刷新。
  • 示例代码
/// 第一种写法
String messageReducer(String msg, Action action) {
  if (action.type == 'shared') {
    return '$msg [shared]';
  }
  return msg;
}

class MessageComponent extends Component {
    MessageComponent(): super(
            view: buildMessageView,
            effect: buildEffect(),
            reducer: messageReducer,
        );
}
/// 第二种写法
Reducer buildMessageReducer() {
  return asReducer(>{
    'shared': _shared,
  });
}

String _shared(String msg, Action action) {
  return '$msg [shared]';
}

class MessageComponent extends Component {
    MessageComponent(): super(
            view: buildMessageView,
            effect: buildEffect(),
            reducer: buildMessageReducer(),
        );
}

//推荐的是第二种写法

3)Effect
Effect顾名思义,用于处理Action的副作用。
Effect用法跟Reducer差不太多,但是作用完全不同。
你可以通过控制effect的返回值来达到某些目的,默认情况下,effect会在reducer之前被执行。
当前effect返回 true 的时候,就会停止后续的effect和reducer的操作
当前effect返回 false 的时候,后续effect和reducer继续执行

  • Effect 是一个处理所有副作用的函数。它接收下面的参数
    • Action action
    • Context context
      • BuildContext context
      • T state
      • dispatch
      • isDisposed
        Effect会接收来自 View 的“意图”,包括对应的生命周期的回调,然后做出具体的执行。 - 它的处理可能是一个异步函数,数据可能在过程中被修改,所以我们应该通过 context.state 获取最新数据。 - 如果它要修改数据,应该发一个 Action 到 Reducer 里去处理。它对数据是只读的,不能直接去修改数据。 - 如果它的返回值是一个非空值,则代表自己优先处理,不再做下一步的动作;否则广播给其他组件的 Effect 部分,同时发送给 Reducer。

4)Adapter

  • 在基础 Component 的概念外,额外增加了一种组件化的抽象 Adapter。它的目标是解决 Component 模型在 ListView 的场景下的 3 个问题
    • 1)将一个"Big-Cell"放在 ListView 里,无法享受 ListView 代码的性能优化。
    • 2)Component 无法区分 appear|disappear 和 init|dispose 事件。
    • 3)Effect 的生命周期和 View 的耦合,在 ListView 的有些场景下不符合直观的预期。
  • 一个 Adapter 和 Component 几乎都是一致的,除了以下几点
    • Component 生成一个 Widget,Adapter 生成一个 ListAdapter,ListAdapter 有能力生成一组 Widget。
      • 不具体生成 Widget,而是一个 ListAdapter,能非常大的提升页面帧率和流畅度。
    • Effect-Lifecycle-Promote
      • Component 的 Effect 是跟着 Widget 的生命周期走的,Adapter 的 Effect 是跟着上一级的 Widget 的生命周期走。
      • Effect 提升,极大的解除了业务逻辑和视图生命的耦合,即使它的展示还未出现,的其他模块依然能通过 dispatch-api,调用它的能力。
    • appear|disappear 的通知
      • 由于 Effect 生命周期的提升,我们就能更加精细的区分 init|dispose 和 appear|disappear。而这在 Component 的模型中是无法区分的。
    • Reducer is long-lived, Effect is medium-lived, View is short-lived.

更多的概念参考官方说明

也可直接查看示例注释

探索

  1. 示例结构如下,是一个简单的待办事项app:
    Flutter示例系列(二)之状态管理-贰(fish-redux)_第4张图片

app.dart 和 main.dart 是项目入口文件,global_store下全局状态相关,todo_list_page下待办事项列表相关,todo_edit_page下编辑事项相关。

在VSCode中搜索插件 fish-redux-template 安装,方便生成模版文件。

  1. 看图分析
    Flutter示例系列(二)之状态管理-贰(fish-redux)_第5张图片
    Flutter示例系列(二)之状态管理-贰(fish-redux)_第6张图片
    通过上面两张截图,可以发现示例分成两个页面(page),即todo_list_page 和 todo_edit_page,todo_list_page 包含公告栏(report_component)、列表(list_adapter+todo_component)和一个悬浮按钮。todo_edit_page 比较简单,只创建一个view展示,但是有一个’Change Theme’按钮,作用是改变整个主题,所以用global_store来管理。

接下来,就要利用插件 fish-redux-template 创建模版文件。

  1. 代码思路
    结合代码一起理解,示例中有大量注释。

1)遵循redux原则,store必须单一,使用 createStore() 创建:

static Store get store => _globalStore ??= createStore(GlobalState(), buildReducer());

示例中有改变主题色的需求(字体大小先忽略),因此定义state:

class GlobalState implements GlobalBaseState, Cloneable {
  //重写GlobalBaseState的属性
  @override
  Color themeColor;
  int themeTextSize;

  //重写Cloneable的方法,在reducer处理action之后,需要获取state对象,通过state对象获取其实例变量
  @override
  GlobalState clone() {
    return GlobalState();
  }
}

改变即是行为,定义action:

enum GlobalAction {
  changeThemeColor,
  changeTextSize,
}

//action生成器
class GlobalActionCreator {
  //如果发出 改变主题色动作,则调用此方法
  static Action changeThemeColor() {
    return const Action(GlobalAction.changeThemeColor);
  }

对应action定义处理函数reducer:

//Reducer 创建格式如下,只需要补充 相应的action以及对应的处理函数
Reducer buildReducer() {
  return asReducer(
    > {
      GlobalAction.changeThemeColor: _changeThemeColor,
      GlobalAction.changeTextSize: _changeTextSize,
    } 
  );
}

2)通过 看图分析 得知,首页面分为三部分,公告栏(report)、 列表栏(todo_list 和 adapter)和悬浮按钮。

2.1)由此可以得出,page的action分为两种,初始化(initTodos ) 和 增加事项(onAdd)。

enum TodoListAction { 
    initTodos, //初始化列表
    onAdd //增加待办事项
}

state默认继承Cloneable,还需要继承GlobalBaseState,用于全局修改主题色。定义List,用于保存数据:

class TodoListState implements GlobalBaseState, Cloneable {
  List todos;

为什么要用onAdd,意味着过程变得复杂,action需要先派发给effect,处理之后再决定是否继续派发给reducer(这种顺序是fish-redux特性)?
因为在effect处做了一个判断,从编辑页面传回的数据如果不完整,则不能添加进list,也不能显示在视图上。

Navigator.of(ctx.context).pushNamed('todo_edit', arguments: null).then((dynamic todo) {
    //从编辑页面返回的 回调,判断下面条件,为true则向reducer派发add行为
    if(todo != null && (todo.title?.isNotEmpty == true || todo.desc?.isNotEmpty == true))  {
      ctx.dispatch(list_action.ListActionCreator.add(todo));
    }
  });
//注意:此刻是传递给adapter。

effect还做了一个操作,即初始化数据,然后,派发给reducer:

//从effect 派发任务到 reducer,effect只负责读取数据,处理数据只能reducer
  ctx.dispatch(TodoListActionCreator.initTodosAction(initTodos));

reducer更新state:

TodoListState _initTodosReducer(TodoListState state, Action action) {
  final List todos = action.payload ?? [];
  final TodoListState newState = state.clone();
  newState.todos = todos;
  return newState;
}

view是纯界面布局,派发action。注意加载公告栏组件和列表组件的方法。
page整合其他文件。

2.2)公告栏组件
公告栏要展示事项总数及完成个数,因此只需state来保存这两个数据:

class ReportState implements Cloneable {
  //总数,以及完成个数
  int total;
  int done;
  //构造方法,默认都为0
  ReportState({this.total = 0, this.done = 0});

view是纯界面布局。
component同page相似。

关键是怎么拿到数据?
fish-redux在page中提供了dependencies,配置了大组件和小组件的连接关系。使用connector可以从大数据中读取小数据,同时小数据的修改同步到大数据。
在todo_list_page下的state中,定义ReportConnector:

class ReportConnector extends Reselect2 {
  @override
  ReportState computed(int sub0, int sub1) {
    
    return ReportState()
    ..done = sub0
    ..total = sub1;
  }

  @override
  int getSub0(TodoListState state) {
    
    return state.todos.where((TodoState tds) => tds.isDone).toList().length;
  }

  @override
  int getSub1(TodoListState state) {
   
    return state.todos.length;
  }

  @override
  void set(TodoListState state, ReportState subState) {
    throw Exception('Unexpected to set PageState from ReportState');
  }

然后再page中配置dependencies,ReportConnector绑定ReportComponent,并且还配置了adapter组件:

dependencies: Dependencies(
                adapter: NoneConn() + ListA.ListAdapter(),
                slots: >{
                  'report': ReportConnector() + ReportComponent()
                }),

2.3)列表组件
使用fish-redux提供的adapter:

class ListAdapter extends DynamicFlowAdapter {
  ListAdapter()
      : super(
          pool: >{
            'todo': TodoComponent(),
          },
          connector: _ListConnector(),
          reducer: buildReducer(),
        );
}

列表有 增加 和 删除 的功能,但是 list_adapter下的action只有add?继续往下看:

enum ListAction { add }

class ListActionCreator {
  static Action add(TodoState state) {
    return Action(ListAction.add, payload: state);
  }
}

别担心,请看reducer,嗯,多了remove?继续往下看:

Reducer buildReducer() {
  return asReducer(
    >{
      ListAction.add: _add,
      todo_action.TodoAction.remove: _remove
    },
  );
}

因为设计的是长按cell删除该行(而不是adapter或者说是列表),所以remove放在todo_component下的action中,但是实际处理应该是adapter的reducer。

2.4)cell组件
在state.dart中定义每条事项的属性:

class TodoState implements Cloneable {

  String uniqueId;
  String title;
  String desc;
  bool isDone;

在action.dart中定义行为:

enum TodoAction { 
  onEdit,
  edit,
  done,
  onRemove,
  remove
 }

在effect.dart中处理 onEdit和 onRemove:

Effect buildEffect() {
  return combineEffects(>{
    TodoAction.onEdit: _onEdit,
    TodoAction.onRemove: _onRemove,
  });
}

在reducer.dart中处理从effect派发的edit,以及从view中派发的done:

Reducer buildReducer() {
  return asReducer(
    >{
      TodoAction.edit: _edit,
      TodoAction.done: _done,
    },
  );
}

view是纯界面布局,派发action。
component同page相似。

2.5)编辑页面(分析大致如上所述)

3.)app.dart
3.1)配置路由,然后使用 connectExtraStore 方法,将page 的 store 连接到 app-store,页面的state同appState始终保持一致。

3.2)示例牵扯到AOP,做一些简单了解。
面向切面编程,主要作用是在不影响原有逻辑的基础上,增添一些常用的功能(比如打印日志、安全检查)。与业务逻辑解耦合,无侵入性。
为此阿里闲鱼团队开源AspectD,AOP for Flutter。

参考博文:
《重磅开源|AOP for Flutter开发利器——AspectD》
关于Middleware

思考?

1.为什么引入material.dart 时,要 hide Action?
因为要使用 fish_redux提供的Action,而非 material.dart 的,避免冲突。

总结

本文主要讲述 fish-redux的起源以及主要用法,并详细分析了示例代码。结合Demo注释会更加清晰,动手尝试效果更佳。

本文Demo地址
fish-redux 地址

=================================================================
个人博客
Github
个人公众号:Flutter小同学
个人网站

你可能感兴趣的:(#,Flutter,redux,flutter,fish-redux)