Flutter 状态管理:Fish Redux 是如何完成配置组装的?

Posted by nepaul on 2021-11-22

备注:本文首发于「百瓶技术」公众号

cover

「百瓶 App」(各市场搜索「百瓶」体验 🥳)在 2019 年 11 月开始接入 Flutter 时,就选定了闲鱼的 Fish Redux 作为状态管理方案。经过近两年的迭代,已经完成了 80 个以上的 Page 级功能,积累了丰富的使用经验。虽然 GitHub[^2] 迭代更新较少,但是它在 2021-05-18 对外宣布正在进行 「2.0 架构的演进[^3]」以及最近释放的 「Flutter Fish_Redux 3.0起航![^10]」,说明内部还是坚持在业务中使用并且对框架进行持续优化的。

本文主要阐述 Fish Redux 两大方面的知识:

  1. 简单介绍核心概念以及基于 Fish Redux[^2] 的「最佳」研发流程;
  2. 深入剖析配置式组装

(备注:1. 阅读本文需要对 Flutter(v1.17.1) 和 Fish Redux(v0.3.7)(或者 Redux[^1])有简单的了解;2. 文中的很多概念,比如 Redux、Fish Redux 等的详细介绍链接都会统一放到文末「参考」部分)。

核心概念

  • 顾名思义:源自 阿里巴巴闲鱼(Fish)技术团队,站在巨人 Redux 的肩膀上,比如 State、Action、Reducer、Store、Middleware 等概念和 Redux 完全一致。
  • 青出于蓝:解决了 Redux「集中」 和「分治」间的矛盾,并且由框架自动完成从细粒度的 Reducer 到 主 Reducer 的合并过程。
  • 组件三要素:Component = View + Effect(可选) + Reducer(可选) + Dependencies(可选)
    • View(UI 视图):完全由数据驱动,负责 Dispatch 事件(Effect 和 Reducer 具体实现),组件依赖通过 ViewService 标准化调用;
    • Effect:处理 副作用(非修改数据的行为,包括生命周期相关的回调);
    • Reducer:修改数据,并以扁平化的方式通知组件刷新;
    • Dependencies:表达组件之间的依赖关系。

「最佳」研发流程

假设需要完成下图 UI 的页面研发,ProtocolBuffer[^4](没接触过的直接理解为 API 文档即可)也已经设计完毕。

那么,基于 Fish Redux 的最佳研发流程是怎么样的?

UserHomePage UI

简单三步完成复杂页面的协同研发:

  • 首先,进行组件拆分:拆分为五个大的组件(如图框所示,红 - profiles_component、绿 - malls_components、黄 - wallets_component、紫 - banners_component、蓝 - tools_component)。由于 Fish Redux 良好的分治策略,这时候完全可以分配给五个同学分别去完成单组件的开发(此时整个页面的目录结构如下图)。
    Code Structure UserHomePage
  • 其次,五位同学进行单组件的开发(以 蓝 - tools_component 为例)
    • state.dart:结合 ProtocolBuffer[^4] 和 UI ,定义 State 结构;
    • reducer.dartaction.dart:分析 state 的哪些数据会被改变,定义好相应的 Action 和 Reducer;
    • effect.dartaction.dart:分析有哪些 副作用 事件响应(比如 请求数据、页面跳转等),定义好相应的 Action 和 Effect;
    • view.dart:视图实现;
    • component.dart:配置组装,这一步通常由 插件[^6] 自动生成了,当然 「最佳实践」 建议删除没有用到的模块(比如很多组件的 dependencies)。
  • 最后,Page 级组装(后文会结合源码深入剖析)
    • user_home_page/state.dart:1. 以子组件的 State(比如 ProfilesStateToolsState)为核心定义 State;2. 建立 State 的 Connector。
    • user_home_page/page.dart:配置组装整个页面。

配置组装

Fish Redux 通过 声明式配置 来将三大核心要素(View、Reducer、Effect)和依赖的子项 dependencies(子组件、middleware、adapter)完成自动组装。
而且大部分代码都可以通过编辑器的 插件[^6] 自动生成,唯一需要手动编写的就是 dependencies 部分。这是非常重要的一个优点,但是业内讲得比较少。
备注:配置组装的代码跟 FishRedux Example 基本一致,可以查看相关代码 [^5] 加深理解。

接下去,将重点阐述配置组装的两个最核心的流程:注册 和 首次页面渲染。

注册

一句代码完成用户中心页面的注册。
code-snippet-register-page

connecrtExtraStore 主要是完成一些全局状态的链接,重点看一下 UserHomePage() 部分。

code-snippet-page-code

UserHomePage() 「类图」

  • UserHomePage:本身不需要新增任何属性或方法,按照固定范式调用 超类 相关方法完成 「页面级」 的组装,并且在注册时对应一个唯一路由。
  • fish_redux Page()
    • InitState<T, P> _initState:页面初始状态,也是 Page 特有且必要的属性。
    • Widget buildPage(P param):初次页面渲染调用(后文会详细阐述)。
  • fish_redux Component
    • ViewBuilder<T> _view:维护 View 层(buildView 属性)。
  • fish_redux Logic:维护 reducer、 effect、 dependencies(所有的依赖子组建)等属性,核心逻辑处理。

UML class-diagram-userhome-page

dependencies

dependencies 接受两个参数(代码见上文图):

  1. adapter:为了解决 list 相关的性能问题,list 相关组件推荐使用;
  2. slots:页面(或组件)依赖的子组件,这是我们重点要理解的部分。

UHPComponentNames.tools: ToolsConnector() + ToolsComponent()

  • UHPComponentNames.tools:依赖的名字,String 类型表明(最佳实践建议提成一个常量,而不是直接用一个 String)。
  • ToolsConnector():Tools 自己管理 State,通过 ToolsConnector 与 Page State 建立链接,Page State 完全不关心 toolsState 具体有哪些属性(高内聚低耦合)!
    code-snippet-tools-component-state-connector
  • ToolsComponent():继承自 Component,除了没有 InitStatemiddleware 相关(包括 viewMiddlewareeffectMiddlewareadapterMiddleware),其他跟 Page 的组装没有差异。
  • ToolsConnector() +:追溯到 ConnOp 的 mixins - ConnOpMixin 里重载了 「加号」,最终创建了一个 _Dependent(redux_component/dependent.dart[^2]) 实例(_Dependent<K, T>(connector: connector, logic: logic))。

_Dependent 的实例化,核心是创建生成 subReducer = connector.subReducer(logic.createReducer()) (下文「createReducer」相关章节会详细阐述 reducer 的创建过程)
code-snippet-_Dependent-class-construction-code

  1. logic.createReducer():最终调用 abstract class LogiccreateReducer 完成 自身 和 子组件 的 reducer 组合;
  2. connector.subReducer(logic.createReducer()):copy 返回一个新的 Reducer。

至此,Page 基于 state、effect、reducer、view 以及 dependencies 完成了实例化的整个过程。

首次页面渲染

当我们打开 UserHomePage 时,会执行如下 routes.buildPage(即 类 PagebuildPage 方法),接受两个参数分别为 路由名和传入该页面的参数:
code-snippet-open-page-build-page

Class Page - Method buildPage

code-snippet-page-class-build-page-method

  • protectedWrapper: 默认返回 _PageWidget, 也支持自定义的包装(UserHomePage 中传入属性 wrapper ,用的较少不必过多关注)。
  • ❗️_PageWidget: 本质上是一个 StatefulWidget[^8],页面渲染的最核心部分
  • 💡 Lifecycle:Fish Redux 中默认的所有生命周期本质上来源于 Flutter Stateful Widget 的生命周期,Reducer 的生命周期和页面是一致的(initState -> didChangeDependencies -> build……)。

_PageWidget

重点看一下 _PageWidget() 的整个流程(核心要素类图):
class-diagram-page-widget

_PageWidget()initState()build() 相关的执行过程:
sequence-diagram-build-page

_PageWidget() - initState 中最重要的 createStore 的操作:

code-snippet-page-create-store

  • _initState(param): UserHomePage 中定义的 initState 在这里调用,接受路由参数对 State 进行初始化操作。
  • createReducer() 过程中最核心的是 combineReducers
    code-snippet-combineReducers
    • 入参 [protectedReducer, protectedDependenciesReducer]: 1. protectedReducer 就是我们自己在 reducer.dart 文件中定义的 buildReducer(); 2. protectedDependenciesReducer 指的就是 依赖的子组件的 reducer,也是通过类似 combineReducers 的方式生成的(后文「createReducer - 生成子组件的 Reducer」部分再来看一下这个过程 )。
    • combineReducers:会做一些 reducer 的判空过滤,最终返回一个 colsure —— Reducer,在后续的 dispatch 相关操作时会执行。
createReducer - 生成子组件的 Reducer

code-snippet-Dependencies-class

  • slots 就是我们在 page.dart 中定义的依赖子组件, createReducer() 中通过循环遍历创建子组件(createSubReducer())的 reducer 列表,最后通过上文中相同的 combineReducers 以及 combineSubReducers 把 reducer 结合起来。
  • createSubReducer() 最终是执行 connector.subReducer(logic.createReducer()) (redux_component/dependent.dart):
    • logic.createReducer() 其实就是执行 logic.dart 中的 createReducer()
    • connector.subReducer(…) 最终执行 MutableConnsubReducer()
      code-snippet-MutableConn-class
      1. get :上文 ToolsConnector 父子组件状态连接器中自己定义的,让子组件获取相应状态数据;
      2. 根据 action 和 props 生成新的状态,并且与老的 state 进行对比;
      3. 如果有变化 clone 一份新的返回,并且通知 view 改变相应状态,否则直接返回当前 state。
  • combineReducers 与上文一致, combineSubReducers 核心最终还是会调用 上文中的 subReducer
_PageWidget() - build() 相关操作

code-snippet-ComponentWidget-class-build

build() 最终调用链路会到 ComponentWidget, 也是一个 StatefulWidget,主要做了两件事情:

  1. void initState() :根据 store、bus 等创建一个 context,这个 context 会在整个页面周期中使用。比如页面中渲染子组件的操作 —— viewService.buildComponent('your-component-name')
  2. Widget build(BuildContext context): 真正创建视图的地方
    assets/code-snippet-ComponentContext-class-build

以上就是 Fish Redux 整个自动配置组装的过程,也简单说了下首次渲染的部分。

最后

源码解析部分并没有把所有细节都阐述,如果感兴趣的朋友欢迎一起探讨。

有任何问题欢迎关注 「百瓶技术」公众号后发消息提问,会第一时间回复。

参考

[^1]: Redux https://redux.js.org
[^2]: GitHub Fish Redux 源码 (2019-03-06 开源)https://github.com/alibaba/fish-redux
[^3]: 闲鱼技术:Flutter Fish Redux 架构演进 2.0 (2021-05-18)https://mp.weixin.qq.com/s/8vFDLq3WaeImyQNb--8ZyQ)
[^4]: Google Protocol Buffers: https://developers.google.com/protocol-buffers
[^5]: Fish Redux 官方例子:https://github.com/alibaba/fish-redux/tree/master/example
[^6]: Visual Studio Code Fish Redux Template Extension: https://marketplace.visualstudio.com/items?itemName=huangjianke.fish-redux-template
[^7]: Flutter architectural overview: https://flutter.dev/docs/resources/architectural-overview
[^8]: Flutter StatefulWidget: https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html
[^9]: Fish Redux 官方文档:https://github.com/alibaba/fish-redux/blob/master/docs/zh-cn/README.md
[^10]: Flutter Fish_Redux 3.0起航!:https://mp.weixin.qq.com/s/YYyU2yoM61VuApz9FsUxQw


Comments: