Flutter 状态管理

2,525 阅读20分钟

1.基础知识:

StatelessWidget:
无状态的Widget,它无法通过setState设置组件状态进行重绘,它内的属性应该被声明为final,防止改变。

生命周期:
初始化->build进行渲染

StatefulWidget:
有状态的Widget,创建一个StatefulWidget组件时,它同时创建一个State对象,通过与State关联可以达到刷新UI的目的

State:
在Flutter中,Widget和State具有不同的生命周期: Widget是临时对象,用于构建当前状态下的应用程序,而State对象在多次调用build()之间保持不变,允许它们保存信息(状态)

State生命周期:
图片1.png

Widget Tree:
UI组件树,但这个只是一种描述信息,一个配置文件(映射),只有build和rebuild以及remove from the tree,渲染引擎是不认识的

Render Tree:
Element就是Widget在UI树具体位置的一个实例化对象,大多数Element只有唯一的renderObject,但还有一些Element会有多个子节点,如继承自RenderObjectElement的一些类,比如MultiChildRenderObjectElement。最终所有Element的RenderObject构成一棵树,我们称之为渲染树,即
render tree当render tree有变化时,rending层它会随即计算出有变化的部分,然后更新UI树,最终将UI树绘制到屏幕上,这个过程类似于React中的虚拟DOM, Diff算法
解决了一个重要的矛盾:
DOM 操作的性能损耗与频繁进行局部 DOM 操作的矛盾。
图片2.jpg

BuildContext:
本质上是Widget对应的Element,
可以通过context在StatelessWidget和StatefulWidget的build方法中直接访问Element对象。状态管理中的xx.of(context)跨组件获取数据,本质上调用了Element的相关方法,通过遍历Element找到的。

2.为什么要状态管理

我们一开始构建应用的时候,也许很简单,这时候可能并不需要状态管理

图片3.png

随着功能的增加,应用程序将会有几十个甚至上百个状态,当app的交互变得复杂,setState出现的次数便会显著增加,每次setState都会重新调用build方法,这势必对于性能,代码的可阅读性,后续的维护带来影响

图片4.png

Flutter很多优秀的设计都来源于React,对于react来说,同级组件之间的通信尤为麻烦,所以需要把所有需要多个组件使用的state拿出来,整合到顶部容器,进行分发。

Flutter也存在类似的问题,通过状态管理可以实现组件通信、跨组件数据储存,以及UI和业务的分离。

图片5.gif

3.声明式编程思维

如果你是从命令式框架(例如 Android SDK 或者 iOS UIKit)转到 Flutter 应用,那么,你需要开始从一个新的角度来考虑 app 开发了。在命令式框架中改变UI需要指令明确的命令:setTextColor,但不适用于Flutter。

Flutter应用是 声明式 的,这也就意味着 Flutter 构建的用户界面就是应用的当前状态。

图片6.png

说明:当你的 Flutter 应用的状态发生改变时(例如,用户在设置界面中点击了一个开关选项)你改变了状态,这将会触发用户界面的重绘。去改变用户界面本身是没有必要的(例如 widget.setText )—你改变了状态,那么用户界面将重新构建。

4.短时 (ephemeral) 和应用 (app) 状态

短时状态

短时状态 (有时也称 用户界面(UI)状态 或者 局部状态) 是你可以完全包含在一个独立 widget 中的状态。

  • 一个 PageView 组件中的当前页面
  • 一个复杂动画中当前进度
  • 一个 BottomNavigationBar 中当前被选中的 tab

widget 树中其他部分不需要访问这种状态。不需要去序列化这种状态,这种状态也不会以复杂的方式改变。
换句话说,不需要使用状态管理架构(例如 ScopedModel, Redux)去管理这种状态。你需要用的只是一个 StatefulWidget。

应用状态

如果你想在你的应用中的多个部分之间共享一个非短时的状态,并且在用户会话期间保留这个状态,我们称之为应用状态(有时也称共享状态)。
应用状态的一些例子:

  • 用户选项
  • 登录信息
  • 一个社交应用中的通知
  • 一个电商应用中的购物车
  • 一个新闻应用中的文章已读/未读状态

为了管理应用状态,你需要研究你的选项。你的选择取决于你的应用的复杂度和限制。 Flutter没有提供原生的全局状态管理,基本上是需要依赖第三方库来实现。虽然在根控件上使用InheritedWidget也可以实现,同样会带来一些问题,比如状态传递过深等。

没有明确的规则:

没有一个明确、普遍的规则来区分一个变量属于短时状态还是应用状态,有时你不得不在此之间重构。比如,刚开始你认为一些状态是短时状态,但随着应用不断增加功能,有些状态需要被改变为应用状态。

图片7.png

经验原则是: 选择能够减少麻烦的方式 - Redux 的作者 Dan Abramov

5.底层逻辑

Flutter中目前有哪些可以做到状态管理,有什么优缺点?
答:State、 InheritedWidget、 Notification、 Stream 数据流

State:

常用而且使用最频繁的一个状态管理类,它必须结合StatefulWidget一起使用,StreamBuilder继承自StatefulWidget,同样是通过setState来管理状态

State 缺点:

  1. 无法做到跨组件共享数据(这个跨是无关联的,如果是直接的父子关系,我们不认为是跨组件) setState是State的函数,一般我们会将State的子类设置为私有,所以无法做到让别的组件调用State的setState函数来刷新
  2. setState会成为维护的难点,因为啥哪哪都是。 随着页面状态的增多,你可能在调用setState的地方会越来越多,不能统一管理
  3. 处理数据逻辑和视图混合在一起,违反代码设计原则 比如数据库的数据取出来setState到Ui上,这样编写代码,导致状态和UI耦合在一起,不利于测试,不利于复用。
  4. setState是整个Widget重新构建(而且子Widget也会跟着销毁重建),如果页面足够复杂,就会导致严重的性能损耗。建议使用StreamBuilder,原理上也是State,但它做到了子Widget的局部刷新,不会导致整个页面的重建。

InheritedWidget:

它的天生特性就是能绑定InheritedWidget与依赖它的子孙组件的依赖关系,并且当InheritedWidget数据发生变化时,可以自动更新依赖的子孙组件!
利用这个特性,我们可以将需要跨组件共享的状态保存在InheritedWidget中,然后在子组件中引用InheritedWidget即可。
专门负责Widget树中数据共享的功能型Widget,如Provider、scoped_model就是基于它开发的

InheritedWidget 缺点:

  1. 每次更新都会通知所有的子Widget,无法定向通知/指向性通知,容易造成不必要的刷新
  2. 不支持跨页面(route)的状态,意思是跨树,如果不在一个树中,我们无法获取
  3. 数据是不可变的,必须结合StatefulWidget、ChangeNotifier或者Steam使用

总结
InheritedWidget组件特别适合在同一树型Widget中,抽象出公有状态,每一个子Widget或者孙Widget都可以获取该状态,我们还可以通过手段控制rebuild的粒度来优化重绘逻辑。

Notification:

它是Flutter中跨层数据共享的一种机制,注意,它不是widget,它提供了dispatch方法,沿着context对应的Element节点向上逐层发送通知
Notification缺点:

  1. 不支持跨页面(route)的状态,准确说不支持NotificationListener同级或者父级Widget的状态通知
  2. 本身不支持刷新UI,需要结合State使用
  3. 如果结合State,会导致整个UI的重绘,效率底下不科学

Stream:

纯Dart的实现,跟Flutter没什么关系,扯上关系的就是用StreamBuilder来构建一个Stream通道的Widget,像知名的rxdart、BloC、flutter_redux、fish_redux全都用到了Stream的api。

Stream 缺点:

  1. api生涩,不好理解
  2. 需要定制化,才能满足更复杂的场景
  3. 缺点恰恰是它的优点,保证了足够灵活,你更可基于它做一个好的设计,满足当下业务的设计。

6.状态管理方案有哪些?

Flutter状态管理方案目前有很多种,有官方推荐的,也有优秀的三方框架,分类如下:

  1. Flutter 本身支持:
    State、 InheritedWidget、 Notification、 Stream 数据流
  2. 官方推荐:
    Provider
    Redux
    BLoC/Rx
    MobX
  3. 三方优秀框架:
    scoped_model
    闲鱼Fish-Redux

7.状态管理方案分析之

scoped_model:

Scoped_model是一个dart第三方库,提供了让您能够轻松地将数据模型从父Widget传递到它的后代的功能。此外,它还会在模型更新时重新渲染使用该模型的所有子项。

它直接来自于Google正在开发的新系统Fuchsia核心Widgets 中对Model类的简单提取,作为独立使用的独立Flutter插件发布。

实现原理
Scoped model使用了观察者模式,将数据模型放在父代,后代通过找到父代的model进行数据渲染,最后数据改变时将数据传回,父代再通知所有用到了该model的子代去更新状态。
而我们则需要将它们放在顶层入口MaterialApp之上,基于 InheritedWidget就能进行全局的状态管理了。

使用步骤
1.添加依赖 2.创建Model(继承Model)3.将Model放入顶层 4.顶层使用ScopedModel包裹 5.在子页面中获取Model:ScopedModelDescendant

优缺点:
scoped_model其实是将InheritedWidget简单的封装了一下,因此它继承了InheritedWidget应有的优点和缺点

优点:自动订阅,自动通知,简单易用
缺点:无法定向通知/指向性通知,无法分离视图逻辑和业务逻辑
另外由于Model必须继承至Model类,具有了侵入性。

常见问题:

* 1. 这里看上去似乎只添加了一个model,我应该如何添加多个model
* 使用Mixinclass MainModel extends Model with AModel,BModel,CModel{}
*
* 2.Scoped是如何做到同步不同页面中的状态的
* abstract class Model extends Listenable {
* Model实现了Listenable接口,并重写了void addListener(VoidCallback listener),removeListener(VoidCallback listener)方法,实现了观察者模式。
* 每当我们调用notifyListeners()方法时,将会通知观察者更新状态。
*
* 3.Scoped如何做到数据能够互相共享的
* 在不同页面间的数据传递使用了InheritedWidget。
*
* 4.侵入性
* 由于Model必须继承至Model类,所以它就具有了侵入性。以后假如不用scoped进行状态管理那么必然会带来需要更改多处代码的情况。这并不是我们希望看到的结果。

BLoC:

BLoC代表业务逻辑组件(Business Logic Component),由来自Google的两位工程师 Paolo Soares和Cong Hui设计

BLoC是一种利用reactive programming方式构建应用的方法,这是一个由流构成的完全异步的世界。BLoC能够允许我们分离业务逻辑!不用考虑刷新屏幕的时机。

原理:

  • 用StreamBuilder包裹有状态的部件,streambuilder将会监听一个流
  • 这个流来自于BLoC
  • 有状态小部件中的数据来自于监听的流。
  • 用户交互手势被检测到,产生了事件。例如按了一下按钮。
  • 调用bloc的功能来处理这个事件
  • 在bloc中处理完毕后将会吧最新的数据add进流的sink中
  • StreamBuilder监听到新的数据,产生一个新的snapshot,并重新调用build方法
  • Widget被重新构建

BLoC的用法:
1.创建BLoC 2.创建BLoC实例 3.在页面中使用StreamBuilder

图片8.png

bloc是一个优秀的状态管理方式,它在处理大量异步事件以及分离业务逻辑上表现很优秀,方便后期维护拓展,但是在共享状态上还有一些缺陷,另外它对于资源的释放并不能很好的支持,需要使用statefulWidget支持

Provider:

Provider是官方文档的例子用的方法. Google 比较推荐的用法. 和BLoC的流式思想相比, Provider是一个观察者模式, 状态改变时要notifyListeners().

Provider的实现在内部还是利用了InheritedWidget,允许将有效信息传递到组件树下的小组件. Provider的好处: dispose指定后会自动被调用, 支持MultiProvider.

Provider从名字上就很容易理解,它就是用于提供数据,无论是在单个页面还是在整个app 都有它自己的解决方案,可以很方便的管理状态。

常用概念:

  1. ChangeNotifier:系统提供的被观察者,数据model需要继承
  2. Provider:订阅者,只用于数据共享管理,提供给子孙节点使用,UpdateShouldNotify Function,用于控制刷新时机
  3. ChangeNotifierProvider:订阅者,不仅能够提供数据供子孙节点使用,还可以在数据改变的时候通知所有消费者。 Model变化后会自动通知ChangeNotifierProvider(订阅者),ChangeNotifierProvider内部会重新构建InheritedWidget,而依赖该InheritedWidget的子孙Widget就会更新.
  4. MultiProvider:多个订阅者:实际上就是通过每一个provider都实现了的 cloneWithChild方法把自己一层一层包裹起来。
  5. Consumer:消费者,能够在复杂项目中,极大地缩小你的控件刷新范围。最多支持6中model
  6. Selector: 消费者,强化的Consumer,支持过滤刷新

使用流程:

  1. 添加依赖
  2. 创建数据 Model
  3. 创建顶层共享数据
  4. 顶层Provider包裹
  5. 在子页面中获取状态

Provider种类:

  1. Provider:只能提供恒定的数据,不能通知依赖它的子部件刷新
  2. ListenableProvider: 提供的对象是继承了 Listenable 抽象类的子类,必须实现其 addListener / removeListener 方法,通常不需要
  3. ChangeNotifierProvider: 对子节点提供一个继承/混入/实现了ChangeNotifier的类,只需要在Model中with ChangeNotifier ,然后在需要刷新状态时调用 notifyListeners 即可
  4. ValueListenableProvider: 提供实现了继承/混入/实现了ValueListenable的Model,实际上是专门用于处理只有一个单一变化数据的ChangeNotifier。
  5. StreamProvider: 专门用作提供(provide)一条 Single Stream。
  6. FutureProvider:提供了一个 Future 给其子孙节点,并在 Future 完成时,通知依赖的子孙节点进行刷新

小结:
本质上:
prvioder通过inheritedElement实现局部刷新,
通过控制自己实现的Element层来更新UI,
通过Element提供的unmount函数回调dispose,实现选择性释放,
其核心类: InheritedProvider

Provider 不仅做到了提供数据,而且它拥有着一套完整的解决方案,覆盖了你会遇到的绝大多数情况。就连 BLoC 未解决的那个棘手的 dispose 问题,和 ScopedModel 的侵入性问题,它也都解决了。
它能够让你开发出简单、高性能、层次清 的应用。

不足之处:Flutter Widget 构建模式很容易在 UI 层面上组件化,但是仅仅使用 Provider,Model 和 View 之间还是容易产生依赖。只有通过手动将 Model 转化为 ViewModel 这样才能消除掉依赖关系。

Redux:

Redux是一种单向数据流架构,可以轻松开发,维护和测试应用程序,也是google推荐的状态管理方式。

原理:

  • 所有的状态都存储在Store里。这个Store会放在根Widget.
  • View拿到Store的状态数据会映射成视图渲染.
  • Redux不直接让view操作数据,通过dispatch一个action通知Reducer,状态变更
  • Reducer接收到这个action,根据action状态,生成新的状态,并替换在Store的旧状态.
  • Store存储了新的状态后,就通知所有使用到了这个状态的View更新(类似setState)。这样我们就能够同步不同view中的状态了.

注意:Store更新状态的时候,并不是更改原来的状态对象,而是直接将reducer生成的新的状态对象替换掉老的状态对象。所以,我们的状态应该是immutable的。

Redux相关概念:

  • State:数据model
  • Store 仓库:整个APP的顶层,存储和管理state
  • Action 动作:通过发起一个Action来告诉Reducer该更新状态了
  • Reducer 还原:根据Action产生新的状态
  • StoreProvider: 一个InheritedWidget,内部存储了一个Store。(数据中心)最顶层必须是 StoreProvider 开始
  • StoreConnector: 连接器:需要两个泛型
    1、一个是我们创建的 State(ReduxState)
    2、一个是 ViewModel,ViewModel决定了converter(转换函数)那边的返回值类型
    同时提供了一个StoreStreamListener,本质上是一个StreamBuilder
  • StoreConverter:转换器:类似于Selector中的selector,转换成本Widget想要的数据
  • StoreStreamListener: 通过监听自己的Stream来完成视图的重建。
  • StoreBuilder:功能同StoreConnector,StoreConnector主要是有个数据转化的作用,可以对数据先做一些转化操作再赋值到组件上,StoreBuilder是直接将数据给显示在组件上
  • middleware 中间件:类似拦截器,作用域位于reducer更新状态之前,本质上也是一个函数。
  • 比如当前是添加用户动作,但是我想在添加用户这操作的前面再做一步其他的动作(异步 action ,action 过滤,日志输出,异常报告等),这时候就可以使用中间件middleware,实现MiddlewareClass该类就行。
  • 中间件的call方法中有个关键方法next(),大多数情况需要调用,否则中间件的链条断了,后面的中间件和Reducer就不执行了。
  • Dispatcher:如何通知状态更新呢?通过store.dispatch

Redux页面更新流程
图片9.png

Redux使用流程:

  1. 添加依赖
  2. 创建State
  3. 创建action
  4. 创建reducer
  5. 创建store
  6. 将Store放入顶层
  7. 在子页面中获取Store中的state
  8. 发出action

优点:

  • 自动订阅
  • 自动通知
  • 可以定向通知
  • 视图和业务逻辑分离

Redux 的缺点:

  • Redux 核心仅仅关心数据管理,不关心具体什么场景来使用它,这是它的优点同时也是它的缺点.
  • 在我们实际使用 Redux 中面临两个具体问题.
  • Redux 的集中和 Component 的分治之间的矛盾.
  • Redux 的 Reducer 需要一层层手动组装,带来的繁琐性和易错性.

Fish-Redux:

Fish Redux 的灵感主要来自于 Redux、React、Elm、Dva 这样的优秀框架,而 Fish Redux 站在巨人的肩膀上,将集中,分治,复用,隔离做的更进一步。

分层架构图

图片10.png

Fish Redux 的改良:

  • 多了一些新概念:Adapter、Component。
  • redux本身只提供一种全局状态管理方案,并不关心具体业务,Fish Redux 通过 Redux 做集中化的可观察的数据管理。
  • fish_redux是针对业务方对redux又进行了一次使用层面的改良:每个组件(Component)需要定义一个数据(Struct)和一个Reducer。同时组件之间的依赖关系解决了集中和分治的矛盾。
  • 同时对 Reducer 的手动层层 Combine 变成由框架自动完成,简化了使用 Redux 的困难。

常用概念:

  • Store: 仓库,存储管理全局的状态(state)
  • State:页面状态和数据
  • Action:发生的动作
  • Effect:接收处理的Action,也包括对生命周期的回调。以 on{Verb} 命名,不修改数据,它对数据是只读的,如果要修改,应该发送一个Action到Reducer中去处理。主要处理副作用操作,比如显示弹窗,网络请求,数据库查询等操作。
  • Reducer:接收处理Action返回新state,以{verb} 命名,一个上下文无关的pure function。
    简单地理解为,reducer是负责(state)的更新,effect 负责 state 更新之外的事情。
  • asReducer:将同个组件的各个Reducer 组合成一个大的 Reducer ,并提供给组件
  • View:负责展示
  • Component:对局部的展示和功能的封装,三要素:View、Effect、Reducer。
  • 对功能细分为修改数据的功能(Reducer)和非修改数据的功能(副作用Effect)。
  • 是 Fish Redux 最基本的元素,其实page也是基于Component的,对比page:1. 没有自己的initState方法 2. 没有办法直接使用,需要使用Connector与父类挂载使用。
  • Adapter:适配器,由于Flutter中ListView的高频使用,fish_redux对ListView做了性能优化,Adapter由此出现。
  • Connector:连接(描述了主页面的state与页面中的Component的关系),从page state中存取与之关联的Component的状态。
  • ViewService:是一个包含Context的viewService,主要是进行一些adapter、component、dialog等组件的组装。
  • Page: 对以上内容组装描述,在Component的基础上增强了aop能力,以及自有state。 page的具备initState()方法而component没有。
  • AOP:middleware ,viewMiddleware, effectMiddleware, adapterMiddleware

Fish-Redux总结:

优点:

  1. 数据集中管理,框架自动完成reducer合并。
  2. 组件分治管理,组件之间以及和容器之间互相隔离。
  3. View、Reducer、Effect隔离。易于编写复用。
  4. 声明式配置组装。
  5. 良好的扩展性。

最大的特点是配置式组装:
一方面将一个大的页面,对视图和数据层拆解为互相独立的 Component|Adapter,上层负责组装,下层负责实现;
另一方面将 Component|Adapter 拆分为 View、Reducer、Effect 等相互独立的上下文无关函数。
所以它会非常干净,易维护,易协作。

缺点:

  1. 框架设计比较重,适用于复杂的业务场景,加上复杂的目录结构以及相关概念,不太适合普通的数据不太复杂的业务。
  2. 学习成本比较高,虽然方案比较牛,各种支持,但是在实际使用中如果不能很好的使用,会影响开发效率。

8.状态管理总结&思考

如何选择框架?

没有哪一种框架可以适配所有的情况,也没有一种框架可以永远适用.
应该根据业务分析适合哪一种,当业务变化时,代码也需要跟着进化,以适配业务的发展.从一开始就介入fish_redux这样的框架,成本高,难度大,只是为了实现一些简单的二级,三级页面,并不是一个好的选择。

选型原则

  • 侵入性
  • 扩展性
  • 高性能
  • 安全性
  • 驾驭性
  • 易用性
  • 范围性

所有的框架都有侵入性,你同意吗?
目前侵入性比较高的代表ScopedModel,如果你选择的框架只能使用它提供的几个入口,可以放弃使用它。

高性能:也是很重要的,这个需要明白它的原理,看它到底如何做的管理。

安全性:也很重要,看他数据管理通道是否安全稳定。

驾驭性:你说你都不理解你就敢用,出了问题找谁?如果驾驭不了也不要用。

易用性:大家应该都明白,如果用它一个框架需要N多配置,N多实现,放弃吧,不合适。简单才是硬道理。

范围性 :这个特点是flutter中比较明显的,框架选型一定要考虑框架的适用范围,到底是适合做局部管理,还是适合全局管理,要做一个实际的考量。

多种状态管理框架是否可以同时使用?

当然可以,你用了redux,就不允许setstate()了? 显然不是.
如何同时使用不同的框架能满足你的需求,使你的性能更好,使用更方便,可读性更强那就使用吧

Q&A

flutterDemo