Fish-Redux是Redux的因地制宜改良版,所以如果是不了解Redux的朋友,可以先去看看Redux的基本概念。我这里以我浅薄的理解讲Fish-Redux的基本概念。
Page
Page继承于Component,用来构建页面,每个页面都有一个Page并且有一个store。在这里初始化store,配置Middleware,对Redux做AOP管理。(AOP是面向切面编程,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。)
例:
class RepoPage extends Page> {
RepoPage()
: super(
initState: initState,
effect: buildEffect(),
reducer: buildReducer(),
view: buildView,
dependencies: Dependencies(
adapter: null,
slots: >{
'InfoComponent':
InfoConnector()+RepoInfoComponent(),
'ActivityComponent':
ActivityConnector()+RepoActivityComponent(),
'CommitComponent':
CommitConnector()+RepoCommitComponent()
}),
middleware: >[
],);
@override
ComponentState createState(){
return TabComponent();
}
}
State
1、定义全局state数据
使用class来声明全局的state,并在声明Page时注入(如上例的Page里注入RepoState)。
例:
class RepoState implements Cloneable {
int tabIndex=0;
List tabList = ["信息","文件","提交","活动"];
TabController tabController;
String repo_name;
String repo_owner;
//RepoHeader
RepoHeader RepoDetail;
//RepoInfo
RepoInfo InfoDetail;
//Activity
List activity_items=new List();
//Commit
List commit_items=new List();
@override
RepoState clone() {
return RepoState()
..tabIndex=tabIndex
..tabList=tabList
..tabController=tabController
..RepoDetail=RepoDetail
..InfoDetail=InfoDetail
..activity_items=activity_items
..commit_items=commit_items
..repo_name=repo_name
..repo_owner=repo_owner;
}
}
- state是不可变的,上面有个 clone 方法,用来获取新的state,在 reducer 中会用到用来 merge state。
- 需要注意的是,state 必须继承自Cloneable,否则注册不上 reducer。
- 如果没有特殊场景需要,在 clone 中要把所有的 state 写全,不然当在 reducer 中使用 clone 方法进行 merge 数据的时候,会丢掉里面缺少的数据。(reducer通过clone的方式改变state中的值,从而触发对页面的更新)
2、使用Connect给组件分配state
class InfoConnector extends ConnOp{
@override
RepoInfoState get(RepoState state) {
// TODO: implement get
return RepoInfoState()
..InfoDetail=state.InfoDetail
..repo_name=state.repo_name
..repo_owner=state.repo_owner;
}
@override
void set(RepoState state, RepoInfoState subState) {
// TODO: implement set
state.InfoDetail=subState.InfoDetail;
state.repo_owner=subState.repo_owner;
state.repo_name=subState.repo_name;
}
}
- 上例将全局state中的InfoDetail分配给了RepoInfoState,作为组件RepoInfo的state,这种分配方式实现了页面级别的数据池,缓和了数据集中和逻辑分治的矛盾。
- 里面的 set、get 方法会在更新 state 的时候自动调用。
- 这里的 InfoConnector 会在 Page 创建的时候 dependencies 的 slots 中使用,作用是分配 state。
Action
类比于 redux 的 action,当发起 action 的时候,根据 action 名字匹配 reducer,但Fish Redux里多了effect,所以也用来匹配effect,命名上区分一下更好(规范命名真的有助于提高开发效率,Action、Effect、Reducer的函数命名以及之间的调用关系,可能是我遇到的最大阻碍了)
例:
enum RepoAction { setRepoData,dataInit,InfoInit,ActivityInit,CommitInit,setInfoData,setCommitData,setActivityData }
class RepoActionCreator {
static Action setRepoDataAction(RepoHeader someState) {
return Action(RepoAction.setRepoData,payload: someState);
}
static Action setInfoDataAction(RepoInfo someState) {
return Action(RepoAction.setInfoData,payload: someState);
}
static Action setCommitDataAction(List someState) {
return Action(RepoAction.setCommitData,payload: someState);
}
static Action setActivityDataAction(List someState) {
return Action(RepoAction.setActivityData,payload: someState);
}
static Action dataInit(Map args){
return Action(RepoAction.dataInit,payload: args);
}
static Action InfoInit(Map args){
return Action(RepoAction.InfoInit,payload: args);
}
static Action ActivityInit(Map args){
return Action(RepoAction.ActivityInit,payload: args);
}
static Action CommitInit(Map args){
return Action(RepoAction.CommitInit,payload: args);
}
}
- 官方推荐的做法是像上面一样用两个类管理 action,一个枚举类,作为action的集合;一个 ActionCreator 类,在这里面可以约束 payload 类型,在 dispatch 的时候掉用 这个类中的函数并传入 playload。
- 我的代码命名规范不够好,但还算正规effect相关的action以名词开头,reducer相关的以动词开头。标准的命名规范是effet处理的action以on{Verb}开头,reducer处理的以{Verb}命名。我在effect和reducer文件里也有遵守这个规范(强迫症的极力补救)。
Effect
effect是Fish Redux特有的概念,在这里处理发起的 action,是一个处理所有副作用的函数,常用于网络请求,界面点击事件等非数据操作,所有的业务逻辑实现都应该放在effect方法中。
使用时有四点需要注意:
- 接收来自 View 的“意图”,包括对应的生命周期的回调,然后做出具体的执行。
- 它的处理可能是一个异步函数,数据可能在过程中被修改,所以我们应该通过 context.state 获取最新数据。
- 如果它要修改数据,应该发一个 Action 到 Reducer 里去处理。它对数据是只读的,不能直接去修改数据。
- 如果它的返回值是一个非空值,则代表自己优先处理,不再做下一步的动作;否则广播给其他组件的 Effect 部分,同时发送给 Reducer。
例:
Effect buildEffect() {
return combineEffects(
以我们项目中的代码为例,做了两件事:一是监听initState的生命周期,将TabController初始化。二是,网络请求需要获取上级页面传进来的信息,所以initState结束后,发起了一个请求并且同步得到 response,但是 state 是不能在这里进行处理的,所以 ctx.dispatch
了一个 action 去 reducer 中处理。
Reducer
类比 redux 的 reducer,在这里处理发起的 action,用于数据行为,通过Action中的payload来对数据进行浅拷贝,更新 state。是一个上下文无关的 pure function,纯函数也就是说,只要是同样的输入,必定得到同样的输出。由于 Reducer 是纯函数,就可以保证同样的State,必定得到同样的 View。但也正因为这一点,Reducer 函数里面不能改变 State,必须返回一个全新的对象。
reducer方法传入需要修改的数据内容以及原始的state对象,如果发现新的数据与原始数据对比并未改变,则直接返回原始对象,不触发界面刷新。如果数据发生变化,则根据原始state对象clone一份新的对象实例,将变化的数据对象保存到新的state对象并返回,触发界面刷新。命名建议以set、change、modify等更改性质单词作为前缀,表示修改状态数据或内容数据。
例:
Reducer buildReducer() {
return asReducer(
buildReducer 中对 action 与 reducer 进行了映射, 当 dispatch 名为 setRepoData 的 action 时,调用下面的 _setDataAction 方法
上面的 _setDataAction 使用了 RepoState 的 clone 函数复制了一份新的数据,修改之后并 return
需要注意的是,与 Redux 一样。Fish Redux 的 state 也是 immutable 的,并且直接修改不生效,只能进行替换。
View
view 是一个输出 Widget 的上下文无关的函数。其负责视图层的构建,由 state 驱动。它接收下面的参数:T state、Dispatch、ViewService。
它主要包含三方面的信息
视图完全由数据驱动。
视图产生的事件/回调,通过 Dispatch 发出“意图”,但绝不做具体的实现。
-
使用依赖的组件/适配器,通过在组件上显示配置,再通过 ViewService 标准化调用。
其中 ViewService 提供了三个能力
BuildContext context,获取 flutter Build-Context 的能力
Widget buildView(String name), 直接创建子组件的能力
这里传入的 name 即在 Dependencies 上配置的名称。
创建子组件不需要传入任何其他的参数,因为自组件需要的参数,已经通过 Dependencies 配置中(见上例Page的配置信息),将它们的数据关系,通过 connector 确立。ListAdapter buildAdapter(), 直接创建适配器的能力。(适配器的用法见下文)
例:
Widget buildView(RepoState state, Dispatch dispatch, ViewService viewService) {
dispatch(RepoActionCreator.dataInit({"name":state.repo_name,"owner":state.repo_owner}));
dispatch(RepoActionCreator.InfoInit({"name":state.repo_name,"owner":state.repo_owner}));
dispatch(RepoActionCreator.ActivityInit({"name":state.repo_name,"owner":state.repo_owner}));
dispatch(RepoActionCreator.CommitInit({"name":state.repo_name,"owner":state.repo_owner}));
String repo_owner=state.repo_owner;
String repo_name=state.repo_name;
TabController _tabController=state.tabController;
var _tabIndex=state.tabIndex;
//省略项目中部分代码
if(repo_descrption==null){...}
else{
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, boxIsScrolled) {
return [
SliverAppBar(...),//TabBar,SliverAppBar
];//[]
},
body: TabBarView(controller: _tabController, children: [
viewService.buildComponent("InfoComponent"),
Container(
child: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: 30,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text('item'));
})),
viewService.buildComponent('CommitComponent'),
viewService.buildComponent("ActivityComponent"),
])),
);
}
}
- 页面构造时调用 dispatch 事件,在对应的 effect 中发送网络请求,并在effect中调用 dispatch 事件,在对应的 reducer 中进行数据更新。
- 通过viewService.buildComponent()创建自定义的子组件,与 Dependencies 上配置的名称一致。
Component
每一个 Component 都是一个组件,组件是对视图展现和逻辑功能的封装。它对视图(view),修改数据(reducer), 非修改数据操作(effect)这三个逻辑进行了剥离。Component = View + Effect(可选) + Reducer(可选) + Dependencies(可选)
例:
class RepoActivityComponent extends Component {
RepoActivityComponent()
: super(
view: buildView,
dependencies: Dependencies(
adapter: NoneConn() + ListItemAdapter(),
slots: >{
}),);
}
这是一个自定义组件,里面使用了view和dependencies两个配置项目,因本项目不涉及数据改变,故统一在page里获取数据,分配给下级组件。组件只做列表展示用。
Dependencies
我们以显式配置的方式来完成大组件所依赖的小组件、适配器的注册,这份依赖配置称之为 Dependencies。它包含两个属性 adapter 和 slots。
- adapter: 组件依赖的具体适配器(用来构建高性能的 ListView)。
- slots:组件依赖的插槽。
- adapter 和 所依赖的组件 会在
view
中被ViewService.buildComponent
调用使用
Adapter
我们在基础 Component 的概念外,额外增加了一种组件化的抽象 Adapter。它的目标是解决 Component 模型在 ListView 的场景下的 3 个问题:
- 将一个"Big-Cell"放在 ListView 里,无法享受 ListView 代码的性能优化。
- Component 无法区分 appear|disappear 和 init|dispose 事件。
- 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.
Adapter的用法比较繁琐,我给的参考文档里有学习的demo,相信通过这几个文档,可以让大家了解Adapter的用法。这里给个文件目录参考:
Connector
它表达了如何从一个大数据中读取小数据,同时对小数据的修改如何同步给大数据,这样的数据连接关系。它是将一个集中式的 Reducer,可以由多层次多模块的小 Reducer 自动拼装的关键。它大大降低了我们使用 Redux 的复杂度。我们不再关系组装过程,我们关系核心的什么动作促使数据怎么变化。它使用在配置 Dependencies 中,在配置中我们就固化了大组件和小组件之间的连接关系(数据管道),所以在我们使用小组件的时候是不需要传入任何动态参数的。
例:
//在page配置里
class RepoPage extends Page> {
RepoPage()
: super(
initState: initState,
effect: buildEffect(),
reducer: buildReducer(),
view: buildView,
dependencies: Dependencies(
adapter: null,
slots: >{
'InfoComponent':
InfoConnector()+RepoInfoComponent()
}),
middleware: >[
],);
}
//在全局state里
class InfoConnector extends ConnOp{
@override
RepoInfoState get(RepoState state) {
// TODO: implement get
return RepoInfoState()
..InfoDetail=state.InfoDetail
..repo_name=state.repo_name
..repo_owner=state.repo_owner;
}
@override
void set(RepoState state, RepoInfoState subState) {
// TODO: implement set
state.InfoDetail=subState.InfoDetail;
state.repo_owner=subState.repo_owner;
state.repo_name=subState.repo_name;
}
}
Lifecycle
默认的所有生命周期,本质上都来自于 flutter State中的生命周期。
在组件内,Reducer 的生命周期是和页面一致的,Effect 和 View 的生命周期是和组件的 Widget 一致的。在适配器中,Reducer 的生命周期是和页面一致的,Effect 的生命周期是和 ListView 的生命周期一致,View 的生命周期是短暂的(划入不可见区域即销毁)。同时增加了 appear 和 disappear 的生命周期, 代表这个 adapter 管理的视图数组,刚进入显示区和完全离开显示区的回调。
总结
附上我自己画的一张草图:
简单来说这几个文件之间的关系就是:
Action定义了一系列的方法,里面有Effect的方法也有Reducer的方法,用户只接触得到View,View通过调用Action的方法通知State发生改变,Effect负责处理业务逻辑相关的请求,Reducer负责处理数据显示相关的请求,它俩各司其职不能越权,所以Effect处理完业务逻辑之后,如果需要对数据进行操作,就再通过调用Action中的Reducer相关方法,让Reducer去处理,数据改变就会触发View的更新。写代码的时候记得这几个文件各司其职就好了。复杂点除了Page就会涉及Component和Adapter,但其实就是大页面套小页面,通过配置文件进行组装就好,本质还是文件之间的调用关系要各司其职。
我认为Fish Redux和MVP、MVVM、MVC一样,是一种代码规范,是团队协作、代码复用、解耦的工具,严格遵守规范写出的代码看起来真的很爽,写过耦合度高、杂乱无章的代码,才能感受到:好的代码风格真的很优美。或许刚上手会有点麻烦,学习成本较高,但我认为非常值得,技术学习其实是相通的,只有多学习优秀开源框架的精妙设计,多学多用多类比,才能融会贯通,提高自己解决问题的能力,站在巨人的肩膀上才能看得更远。
我对于Fish-Redux的使用也仅限于我们项目,原理性的东西了解不多,更复杂的情况可能也未涉及,附上我学习Fish-Redux的参考博客,大家有兴趣的可以看看。
https://www.jianshu.com/p/a76b4a2b426e
https://juejin.cn/post/6844903856938319879#heading-11
https://blog.csdn.net/YYWX/article/details/105900669
https://mp.weixin.qq.com/s?__biz=MzU3Mzc2NDY1MQ==&mid=2247483973&idx=1&sn=bbe75c7e44161f619d16892fb88d1fd2&chksm=fd3de701ca4a6e171658e9774dc26a2dff324020a4d6379f081b69abd263212941a44d214b5b&token=1357307897&lang=zh_CN&scene=21#wechat_redirect