Fish-Redux使用指南(比官方文档更友好)

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(>{
    Lifecycle.initState: _initController,
    RepoAction.dataInit:_onInit
  });
}
void _onInit(Action action, Context ctx) async {
  final repo_name=action.payload['name'];
  final repo_owner=action.payload['owner'];
  String Repobody = RequestBodyHelper.getRepo(repo_name,repo_owner);
  DioManager.getInstance().post("graphql",
      {
        "query":Repobody,
        "variables":{},
      },
          (data){
        RepoHeader rh = new RepoHeader.fromJson(data["data"]["repository"]);
        ctx.dispatch(RepoActionCreator.setRepoDataAction(rh));
      },
          (error){
        print(error);
      });
}

以我们项目中的代码为例,做了两件事:一是监听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(
    >{
      RepoAction.setRepoData: _setDataAction
    },
  );
}

RepoState _setDataAction(RepoState state, Action action) {
  final RepoState newState = state.clone();
  newState.RepoDetail=action.payload;
  return newState;
}
  • 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

你可能感兴趣的:(Fish-Redux使用指南(比官方文档更友好))