[闲鱼技术] Flutter React编程范式实践

做者:闲鱼技术-匠修前端

Flutter Widget的设计灵感来源于React,是一款原生就立足于响应式的UI框架。本文基于Flutter特色,试图结合闲鱼在Flutter的工程应用来谈下咱们对Flutter React编程范式的思考和践行。react

Reactive的诞生

谈起UI总会讲到MVC,它出现的时间很早,那时候尚未普及现代GUI普遍使用的事件驱动(消息循环)模型,因此很长的时间内,MVC都在进化,不断的被从新定义。到如今MVC已是一个很宽泛的概念了。使用基础的MVC做为框架来开发容易出现模块职责边界模糊,逻辑调用方向混乱。GUI框架进化后,将用户事件的分发处理集成到了View模块中,由此出现了MVP,MVP职责划分较清晰,逻辑调用方向也比较好把握,可是很繁琐,开发效率不高。再随着Web的发展,标记语言被应用于界面描述,开始出现逻辑界面分离和无状态化界面,MVVM应运而生。MVVM让架构层面来提供数据和View的双向绑定,减轻了开发工做,但有时候也带来了必定程度的状态混乱。函数式编程在近年被从新提起,并引起潮流,催生了响应式界面开发,响应式是对GUI事件驱动模型的一种返璞归真。git

我的对前端架构迭代的理解: github

archs

从迭代历程上看,Model和View是两个相对固定的角色,它们容易理解,也能很好的肯定职责边界。如何去沟通Model和View是架构设计的关键,响应式的通常作法是让Model回到最初的事件驱动,结合函数式的数据流来驱动View刷新。这样有比较清晰的角色划分和简单易于理解的逻辑连接,能较好的统一编程模式。编程

reactive

Flutter的Reactive特性

一般GUI框架都有一些共同点,好比View的树形层级,消息循环,Vsync信号刷新等,Flutter也继承这些经典的设计,可是Flutter并无使用标记语言来描述界面(例如Web中的HTML,Android中的XML),这其中有Flutter立足于响应式的初衷。Reactive是一款将事件数据流做为核心的开发模型,UI框架会提供相应的特性来提供更好的支持。redux

1.描述界面而不要操做界面

有一种说法认为函数式语言和命令式语言的不一样在于命令式语言是给计算机下达指令而函数式语言是向计算机描述逻辑。这种思路在Flutter UI中获得了体现。Flutter不提倡去操做UI,它固然也基本不会提供操做View的API,好比咱们常见的相似TextView.setText(),Button.setOnClick()这种是不会有的。对界面的描述是能够数据化的(相似XML,JSON等),而对界面的操做是很难数据化的,这很重要,响应式须要方即可持续的将数据映射成界面。 api

dataflow
在Flutter中用Widget来描述界面,Widget只是View的“配置信息”,编写的时候利用Dart语言一些声明式特性来获得相似结构化标记语言的可读性。不论Stateless Widget 仍是 Stateful Widget都是不可变的(immutable),其中的成员变量也应该都是final的,也就是说,Widget是“只读”的。Widget是数据的映射,当数据改变的时候,咱们须要从新建立Widget去更新界面,这意味着Widget会建立销毁的很是频繁,不过Flutter使用的Dart虚拟机能高效的处理这种短周期的轻量对象。

这种设计思路对刚接触的开发者可能有些不习惯,咱们能够借助开发Android中的ListView(iOS中的TableView)来理解:咱们一般先准备好一个数据List,而后实现一个Adapter来将List中的items映射成一个个itemView,最后将List和Adapter设置给ListView。这样当咱们改变List中的数据,ListView就会相应的刷新View。Flutter相似,咱们准备好Widgets(只不过Widget的“容器”是Tree而不是List),Flutter会提供Adapter(RenderObjectToWidgetAdapter)将其映射成渲染用的RenderObject,当Widget更新时就会刷新界面。 缓存

framework

另外,Widget也能经过设置Key来缓存复用,在相似ListView的场景中,Item Widget的复用是颇有收益的。bash

2.基于共同祖先通讯

在咱们国家,若是你想和别人沟通上拉近距离,有时候会进入到相似“咱们500年前是一家”的这种语境中。在Flutter中,若是两个组件要通讯,也是去找祖先(固然,也有可能两个组件自己就有遗传关系),Flutter把它描述成“数据上行,通知下行”。网络

comm

可是,在一个很是复杂的树形层级中,要找到某位“祖先”并非很容易的事情,并且性能也很差。Flutter为此作了优化,提供了InheritedWidget,“祖先”Widget继承该类型后,child能够经过BuildContext中提供的inheritFromWidgetOfExactType方法方便的找到在层级中离的最近的那位“祖先”。该方法作了优化,效率很高,而且可让child和“祖先”创建依赖关系,方便作刷新。

Flutter中并无提倡相似controller的概念(像Android中的Activity,iOS中的ViewController),自己View是不可操做的,controller也就失去了意义。那么,组件之间的通讯就必须在View层“自力更生”了。

3.函数式数据流

这确定不是Flutter才有的,要想把响应式实现的简洁优雅,就要利用好语言的函数式特性。Flutter的亮点是它使用的Dart语言能把这件事情变的很轻量,你基本不须要引入什么第三方库就能作到(不过确实有RxDart库,但感受只是作了额外的加强),并且明显语言Api的设计也往这个方向上作了优化,很是方便。具体能够看看StreamRxDart

基于React的框架实践

统一状态管理和单向数据流

经过React的实践,响应式能够很好的解决数据到界面的更新,并且效率也不错。可是自身对数据状态的管理不足,React官方提出了Flux,而在面对复杂业务场景时,Flutter官方也是推荐Redux架构,咱们也是根据这一思路搭建的框架。

redux

首先是业务逻辑和界面分离,界面是无状态(Stateless)的,咱们也正在尝试自动化的方法直接生成界面代码,因此Widget中是不会有业务逻辑代码的。当咱们把一个能描述当前界面的数据(State)交给View层时,界面就应该能正常展现。用户和界面交互会产生Action,Action表明了用户交互的意图,Action能够携带信息(好比用户使用输入留言,Action中就应该携带用户留言的内容信息)。Action会输入给Store,Store会经过注册的Interrupters对Action作前期拦截处理,能够经过Interrupter截拦Action,也能够把一个Action从新改写成另外的Action。Store而后收集相应绑定的Reducers对Action作一次reduce操做,产生新的State,并通知界面刷新。

一般咱们在建立Store的时候就组册好Reducer和Interrupter:

Store<PublishState> buildPublishStore(String itemId) {

  //设置状态初始值
  PublishState initState = new PublishState();
  initState.itemId = itemId;
  initState.isLoading = true;

  //建立Reducer和对应Action的绑定
  var reducerBinder = ActionBinder.reducerBinder<PublishState>()
    ..bind(PublishAction.DETAIL_LOAD_COMPLETED, _loadCompletedReducer)
    ..bind(PublishAction.DELETE_IMAGE, _delImageReducer)
    ..bind(PublishAction.ADD_IMAGE, _addImageReducer);

  //建立Interrupter和对应Action的绑定
  var interrupterBinder = ActionBinder.interrupterBinder<PublishState>()
    ..bind(PublishAction.LOAD_DETAIL, _loadDataInterrupter)
    ..bind(PublishAction.ADD_IMAGE, UploadInterruper.imageUploadInterrupter);

 //建立Store	
 return new CommonStore<PublishState>(
      name: 'Publish',
      initValue: initState,
      reducer: reducerBinder,
      interrupter: interrupterBinder);
}
复制代码

Reducer中就是处理用户交互时产生的Action的逻辑代码,接收3个参数,一个是执行上下文,一个要处理的Action,一个是当前的State,处理结束后必须返回新的State。函数式理想的Reducer应该是一个无反作用的纯函数,显然咱们不该该在Reducer中去访问或者改变全局域的变量,但有时候咱们会对前面的计算结果有依赖,这时能够将一些运行时数据寄存在ReduceContext中。Reducer中不该该有异步逻辑,由于Store作Reduce操做是同步的,产生新State后会当即通知界面刷新,而异步产生对State的更新并不会触发刷新。

PublishState _delImageReducer(ReduceContext<PublishState> ctx, Action action, PublishState state) {
  int index = action.args.deleteId;
  state.imageUplads.removeAt(index);
  return state;
}
复制代码

Interrupter形式上和Reducer相似,不一样的是里面能够作异步的逻辑处理,好比网络请求就应该放在Interrupter中实现。

为何会有Interrupter呢?

换一个角度,咱们能够把整个Store当作一个函数,输入是Action,输出的是State。函数会有反作用,有时咱们输入参数并不必定得会相应有输出,好比日志函数( void log(String) ),咱们输入String只会在标准输出上打印一个字符串,log函数不会有返回值。一样,对Store来讲,也不是全部的Action都要去改变State,用户有时候触发Action只要想让手机震动下而已,并不会触发界面更新。因此,Interrupter就是Store用来处理反作用的。*

///截拦一个网络请求的Action,并在执行请求网络后发出新Action
  bool _onMtopReq(InterrupterContext<S> ctx, Action action) {
    NetService.requestLight(
        api: action.args.api,
        version: action.args.ver,
        params: action.args.params,
        success: (data) {
          ctx.store.dispatch(Action.obtain(Common.MTOP_RESPONSE)
            ..args.mtopResult = 'success'
            ..args.data = data);
        },
        failed: (code, msg) {
          ctx.store.dispatch(Action.obtain(Common.MTOP_RESPONSE)
            ..args.mtopResult = 'failed'
            ..args.code = code
            ..args.msg = msg);
        });

    return true;
  }
复制代码

一般咱们会让一个界面根部的InheritedWidget来持有Store,这样界面上的任何Widget 都能方便的访问到Store,并和Store创建联系。这种作法能够参考redux_demo,再此不详细展开。

最后简单的说说Store的实现,Store可以接收Action,而后执行reduce,最后向widget提供数据源。Widget能够基于提供的数据源创建数据流,响应数据变动来刷新界面。这其中最核心的就是Dart的Stream。

......
	
	//建立分发数据的Stream
    _changeController = new StreamController.broadcast(sync: false);

    //建立接收Action的Stream
    _dispatchController = new StreamController.broadcast(sync: false);

    //设置响应Action的函数
    _dispatchController.stream.listen((action) {
      _handleAction(action);
    });
    
    ......
   
   //向Store中分发Action 
	void dispatch(Action action) {
   		_dispatchController.add(action);
  	}
  	
  	//Store向外提供的数据源
  	Stream<State> get onChange => _changeController.stream;
复制代码

Store中最核心的对Action进行reduce操做:

//收集该Action绑定的Reducer
	final List<ReduceContext<State>> reducers = _reducers.values
        .where((ctx) => ctx._handleWhats.any((what) => what == action.what))
        .toList();
       
  	//执行reduce
  	Box<Action, State> box = new Box<Action, State>(action, _state);
   	box = reducers.fold(box, (box, reducer) {
   		box.state = reducer._onReduce(box.action, box.state);
      	return box;
   	});    
   
   	//触发更新
   	_state = box.state;
   	_changeController.add(_state);  
复制代码

Widget基于Store暴露的数据源创建数据流:

store.onChange
      //将Store中的数据转换成Widget须要的数据
     .map((state) => widget.converter(state)) 
     
      //比较前一次数据,若是想等则不用更新界面
     .where((value) => (value != latestValue))
     
     //更新界面
     .listen((value){
          ...
          setState()
          ...
     })
复制代码

组件化的扩展

咱们在业务开发中发现,有时候一个页面一个Store会带来组件复用上的不方便,好比视频播放组件是一个逻辑比较内聚的组件,若是把它的reducer都集中放在页面的Store中那么别的页面想要复用这个开发好的视频组件就不方便了,这时候视频组件可能须要一个独立的Store来存放视频播放相关的逻辑。咱们遵循Flutter组件通讯方法,将框架扩展为容许存在多个Store,而且作到对Widget开发无感知。

compontent

Widget只能感知离它最近的Store持有者,该Store会向更高层级Store转发Action,同时接收来自更高层级Store的数据变动并通知Widget。

延展讨论

相对目前流行的MVVM框架(Vue,Angular)可以细粒度的绑定数据,并实现界面的最小化刷新,Flutter上面尚未找到很好的办法可以在框架内自动实现,目前只能依赖开发者去手动处理。这难免会下降开发效率,拉低开发体验,咱们也在探索更好的方法,若是感兴趣或者有好的解决思路,欢迎和咱们交流。

当遇到状态复杂页面(多动画,多view联动)时,Store中应该要提供相关工具或机制来管理复杂的状态来提升开发效率,状态机是个可选的方案之一。若是有在Dart下优雅的状态机框架实现或思路,请务必和咱们分享一下。

最后,闲鱼技术团队广招各种方向的达人,不管你是精通移动端,前端,后台,仍是机器学习,音视频,自动化测试等,都欢迎投递简历加入咱们,一同用技术改善生活!

简历投递:guicai.gxy@alibaba-inc.com

相关文章
相关标签/搜索