乐趣区

Flutter高内聚组件怎么做阿里闲鱼打造开源高效方案

fish_redux 是闲鱼技术团队打造的 flutter 应用开发框架,旨在解决页面内组件间的高内聚、低耦合问题。开源地址:https://github.com/alibaba/fish-redux

从 react_redux 说起

redux对于前端的同学来说是一个比较熟悉的框架了,fish_redux借鉴了 redux 单项数据流思
想。在 flutter 上说到 redux,大家可能第一反应会类比到react 上的 react_redux。在react_redux 中有个重要的概念——connect

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

简单得说,connect允许使用者从 Redux store 中获取数据并绑定到组件的 props 上,可以 dispatch 一个 action 去修改数据。

那么 fish_redux 中的 connector 是做什么的呢?为什么说 connector 解决了组件内聚的问题?我们应该如何理解它的设计呢?

connector in fish_redux

尽管都起到了连接的作用,但 fish_reduxreact_redux在抽象层面有很大的不同。

fish_redux本身是一个 flutter 上的应用框架,建立了自己的 component 体系,用来解决组件内的高内聚和组件间的低耦合。从 connector 角度来说,如何解决内聚问题,是设计中的重要考量。

fish_redux自己制造了 Component 树,Component聚合了 statedispatch,每一个子 Componentstate通过 connector 从父 Componentstate中筛选。如图所示:

可以看到,fish_reduxconnector 的主要作用把父子 Component 关联起来,最重要的操作是 filterstate 从上之下是一个严谨的树形结构,它的结构复用了 Component 的树形结构。类似一个漏斗形的数据管道,管理数据的分拆与组装。它表达了如何组装一个Component

而对于 react_redux 来说,它主要的作用在于把 react 框架和 redux 绑定起来,重点在于如何让 React component 具有 Redux 的功能。

从图中可以看到,react_reduxReact 是平行的结构,经过 mapStateToProps 后的 state 也不存在严谨的树形结构,即对于一个 React component 来说,它的 state 来自于 Redux store 而不是父 component 的 state。从框架设计的角度来说,react_redux 最重要的一个操作就是attach

源码分析

说完概念,我们从源码的角度来看看 fish_redux 中的 connector 是如何运作的,以 fish_redux 提供的 example 为例。

class ToDoListPage extends Page<PageState, Map<String, dynamic>> {ToDoListPage()
      : super(
          ...
          dependencies: Dependencies<PageState>(adapter: ToDoListAdapter(),
              slots: <String, Dependent<PageState>>{'report': ReportConnector() + ReportComponent()}),
        ...
        );
}

在 ToDoListPage
的构造函数中,向父类构造传递了一个 Dependencies 对象,在构造 Dependencies 时,参数
slots 中包含了名叫 ”report“ 的 item,注意这个 item 的生成,是由一个
ReportConnector+ReportComponent 产生的。

从这里我们得出一个简单却非常重要的结论:

在 fish_redux 中,一个 Dependent = connector + Component。

Dependent代表页面拼装中的一个单元,它可以是一个 Component(通过 buildComponent 函数产
生),也可以是一个 Adapter(由 buildAdapter 函数产生)。这样设计的好处是,对于 View 拼装操作
来说,Dependent对外统一了 API 而不需要透出更多的细节。

根据上面我们得出的结论,connector用来把一个更小的 Component 单元链接到一个更大的
ComponentAdapter上。这与我们之前的描述相符合。

connector 到底是什么

知道了 connector 的基本作用,我们来看一下它到底链接了哪些东西以及如何链接。

先来看一下 ReportConnector
类的定义:

class ReportConnector extends ConnOp<PageState, ReportState>

ReportConnector继承了 ConnOp 类,所有 connector 的操作包括 + 操作,都来自于 ConnOp 类。

set/get

既然是数据管道,就会有 获取 放置

set函数的入参很好得表达了 TP的意思,即把一个 P 类型的 subState 合并到 T 类型的
state 中。

再回头看 get 函数,就很好理解了,get函数表达的就是如何从 T 类型的 state 中获取 P 类型
subStateDependent 使用。

operator +

+操作符的重载是我们最初看到 connector 作用的地方,也是 connector 发挥作用的入口。

LogicComponentAdapter的父类,它表示页面组装元素的逻辑层,里面包含了
reducer/effect/higherEffect 等与逻辑相关的元素以及它的组装过程。

operator+调用了 createDependent 函数,接着会调用到 _Dependent 类的构造函数,这里将
logicconnector放入 _Dependent 内部,在后面 fish_reduxComponent组装的过程中,connector会随着外部对 _Dependent 中函数的调用发挥作用。

connector 正式登场

铺垫了这么多,是该 connector 正式发挥作用的时候了。

get

我们以 Component 为例,会调用到 _DependentbuildComponent函数:

Widget buildComponent(MixedStore<Object> store, Get<T> getter) {
    final AbstractComponent<P> component = logic;
    return component.buildComponent(store, () => connector.get(getter()));
}

这里的 logic 实际就是一个 Component 对象,在调用 ComponentbuildComponent函数的
时候,使用 get 函数从一个大的父 state 中获取到当前 Component 需要的数据集。接下去,这个变换后的子 state 将被用在例如 ViewBuilderRedcuer函数中。

这是 connector 在数据获取上的作用。

set

还是在 _Dependent 类里面,看 createSubReducer 函数:

SubReducer<T> createSubReducer() {
    final Reducer<P> reducer = logic.reducer;
    return reducer != null ? connector.subReducer(reducer) : null;
}

首现从一个 Logic(这里实际上是一个Component) 对象中获取到外部设置进来的 reducer,接着
调用 subReducer 返回一个 SubReducer 对象。SubReducer是一个被 wrap 后的Reducer

subReducer的实现在 MutableConn 中,ConnOp继承了 MutableConn 类,也获得了这个能
力。

SubReducer<T> subReducer(Reducer<P> reducer) {return (T state, Action action, bool isStateCopied) {final P props = get(state);
      if (props == null) {return state;}
      final P newProps = reducer(props, action);
      final bool hasChanged = newProps != props;
      final T copy = (hasChanged && !isStateCopied) 
                  ? _clone<T>(state) : state;
      if (hasChanged) {set(copy, newProps);
      }
      return copy;
    };
}

它首现通过 get 函数得到一个变换后的数据集 props,接着调用原始的reducer 函数进行逻辑处
理,这里有一个优化也是 SubReducer 的作用,如果数据集在经过 reducer 处理之后发生了变化,
并且 state 已经被 copy 过一次了 (isStateCopied==true),就直接把newProps 通过 set 函数
更新到 state 中去。(这个优化可以防止多个子 state 发生变化的时候父 state 被拷贝多次)

至此,connector在数据更新上的作用也体现出来了。

ReportConnector

最后,就好理解 ReportConnector 的实现了:

class ReportConnector extends ConnOp<PageState, ReportState> {
  @override
  ReportState get(PageState state) {final ReportState reportState = ReportState();
    reportState.total = state.toDos.length;
    reportState.done =
        state.toDos.where((ToDoState tds) => tds.isDone).toList().length;
    return reportState;
  }

  @override
  void set(PageState state, ReportState subState) {}}

很明显的,在 get 函数中,ReportStatePageState 中获得了 total/done 字段。

总结

闲鱼客户端的详情页完全使用了 fish_redux 进行了重构,通过高内聚的 Component+connector 形式,使得 Component 可以被大量复用,很好得支持了 5 中类型的详情页。未来我们会基于 fish_redux 强大的扩展能力制作更多组件来满足不同业务对于框架的需求。


本文作者:闲鱼技术 - 海潴

阅读原文

本文为云栖社区原创内容,未经允许不得转载。

退出移动版