1.基础知识:
StatelessWidget:
无状态的Widget,它无法通过setState设置组件状态进行重绘,它内的属性应该被声明为final,防止改变。
生命周期:
初始化->build进行渲染
StatefulWidget:
有状态的Widget,创建一个StatefulWidget组件时,它同时创建一个State对象,通过与State关联可以达到刷新UI的目的
State:
在Flutter中,Widget和State具有不同的生命周期: Widget是临时对象,用于构建当前状态下的应用程序,而State对象在多次调用build()之间保持不变,允许它们保存信息(状态)
State生命周期:
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 操作的矛盾。
BuildContext:
本质上是Widget对应的Element,
可以通过context在StatelessWidget和StatefulWidget的build方法中直接访问Element对象。状态管理中的xx.of(context)跨组件获取数据,本质上调用了Element的相关方法,通过遍历Element找到的。
2.为什么要状态管理
我们一开始构建应用的时候,也许很简单,这时候可能并不需要状态管理
随着功能的增加,应用程序将会有几十个甚至上百个状态,当app的交互变得复杂,setState出现的次数便会显著增加,每次setState都会重新调用build方法,这势必对于性能,代码的可阅读性,后续的维护带来影响
Flutter很多优秀的设计都来源于React,对于react来说,同级组件之间的通信尤为麻烦,所以需要把所有需要多个组件使用的state拿出来,整合到顶部容器,进行分发。
Flutter也存在类似的问题,通过状态管理可以实现组件通信、跨组件数据储存,以及UI和业务的分离。
3.声明式编程思维
如果你是从命令式框架(例如 Android SDK 或者 iOS UIKit)转到 Flutter 应用,那么,你需要开始从一个新的角度来考虑 app 开发了。在命令式框架中改变UI需要指令明确的命令:setTextColor,但不适用于Flutter。
Flutter应用是 声明式 的,这也就意味着 Flutter 构建的用户界面就是应用的当前状态。
说明:当你的 Flutter 应用的状态发生改变时(例如,用户在设置界面中点击了一个开关选项)你改变了状态,这将会触发用户界面的重绘。去改变用户界面本身是没有必要的(例如 widget.setText )—你改变了状态,那么用户界面将重新构建。
4.短时 (ephemeral) 和应用 (app) 状态
短时状态
短时状态 (有时也称 用户界面(UI)状态 或者 局部状态) 是你可以完全包含在一个独立 widget 中的状态。
- 一个 PageView 组件中的当前页面
- 一个复杂动画中当前进度
- 一个 BottomNavigationBar 中当前被选中的 tab
widget 树中其他部分不需要访问这种状态。不需要去序列化这种状态,这种状态也不会以复杂的方式改变。
换句话说,不需要使用状态管理架构(例如 ScopedModel, Redux)去管理这种状态。你需要用的只是一个 StatefulWidget。
应用状态
如果你想在你的应用中的多个部分之间共享一个非短时的状态,并且在用户会话期间保留这个状态,我们称之为应用状态(有时也称共享状态)。
应用状态的一些例子:
- 用户选项
- 登录信息
- 一个社交应用中的通知
- 一个电商应用中的购物车
- 一个新闻应用中的文章已读/未读状态
为了管理应用状态,你需要研究你的选项。你的选择取决于你的应用的复杂度和限制。 Flutter没有提供原生的全局状态管理,基本上是需要依赖第三方库来实现。虽然在根控件上使用InheritedWidget也可以实现,同样会带来一些问题,比如状态传递过深等。
没有明确的规则:
没有一个明确、普遍的规则来区分一个变量属于短时状态还是应用状态,有时你不得不在此之间重构。比如,刚开始你认为一些状态是短时状态,但随着应用不断增加功能,有些状态需要被改变为应用状态。
经验原则是: 选择能够减少麻烦的方式 - Redux 的作者 Dan Abramov
5.底层逻辑
Flutter中目前有哪些可以做到状态管理,有什么优缺点?
答:State、 InheritedWidget、 Notification、 Stream 数据流
State:
常用而且使用最频繁的一个状态管理类,它必须结合StatefulWidget一起使用,StreamBuilder继承自StatefulWidget,同样是通过setState来管理状态
State 缺点:
- 无法做到跨组件共享数据(这个跨是无关联的,如果是直接的父子关系,我们不认为是跨组件) setState是State的函数,一般我们会将State的子类设置为私有,所以无法做到让别的组件调用State的setState函数来刷新
- setState会成为维护的难点,因为啥哪哪都是。 随着页面状态的增多,你可能在调用setState的地方会越来越多,不能统一管理
- 处理数据逻辑和视图混合在一起,违反代码设计原则 比如数据库的数据取出来setState到Ui上,这样编写代码,导致状态和UI耦合在一起,不利于测试,不利于复用。
- setState是整个Widget重新构建(而且子Widget也会跟着销毁重建),如果页面足够复杂,就会导致严重的性能损耗。建议使用StreamBuilder,原理上也是State,但它做到了子Widget的局部刷新,不会导致整个页面的重建。
InheritedWidget:
它的天生特性就是能绑定InheritedWidget与依赖它的子孙组件的依赖关系,并且当InheritedWidget数据发生变化时,可以自动更新依赖的子孙组件!
利用这个特性,我们可以将需要跨组件共享的状态保存在InheritedWidget中,然后在子组件中引用InheritedWidget即可。
专门负责Widget树中数据共享的功能型Widget,如Provider、scoped_model就是基于它开发的
InheritedWidget 缺点:
- 每次更新都会通知所有的子Widget,无法定向通知/指向性通知,容易造成不必要的刷新
- 不支持跨页面(route)的状态,意思是跨树,如果不在一个树中,我们无法获取
- 数据是不可变的,必须结合StatefulWidget、ChangeNotifier或者Steam使用
总结
InheritedWidget组件特别适合在同一树型Widget中,抽象出公有状态,每一个子Widget或者孙Widget都可以获取该状态,我们还可以通过手段控制rebuild的粒度来优化重绘逻辑。
Notification:
它是Flutter中跨层数据共享的一种机制,注意,它不是widget,它提供了dispatch方法,沿着context对应的Element节点向上逐层发送通知
Notification缺点:
- 不支持跨页面(route)的状态,准确说不支持NotificationListener同级或者父级Widget的状态通知
- 本身不支持刷新UI,需要结合State使用
- 如果结合State,会导致整个UI的重绘,效率底下不科学
Stream:
纯Dart的实现,跟Flutter没什么关系,扯上关系的就是用StreamBuilder来构建一个Stream通道的Widget,像知名的rxdart、BloC、flutter_redux、fish_redux全都用到了Stream的api。
Stream 缺点:
- api生涩,不好理解
- 需要定制化,才能满足更复杂的场景
- 缺点恰恰是它的优点,保证了足够灵活,你更可基于它做一个好的设计,满足当下业务的设计。
6.状态管理方案有哪些?
Flutter状态管理方案目前有很多种,有官方推荐的,也有优秀的三方框架,分类如下:
- Flutter 本身支持:
State、 InheritedWidget、 Notification、 Stream 数据流 - 官方推荐:
Provider
Redux
BLoC/Rx
MobX - 三方优秀框架:
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
* 使用Mixin!class 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
bloc是一个优秀的状态管理方式,它在处理大量异步事件以及分离业务逻辑上表现很优秀,方便后期维护拓展,但是在共享状态上还有一些缺陷,另外它对于资源的释放并不能很好的支持,需要使用statefulWidget支持
Provider:
Provider是官方文档的例子用的方法. Google 比较推荐的用法. 和BLoC的流式思想相比, Provider是一个观察者模式, 状态改变时要notifyListeners().
Provider的实现在内部还是利用了InheritedWidget,允许将有效信息传递到组件树下的小组件. Provider的好处: dispose指定后会自动被调用, 支持MultiProvider.
Provider从名字上就很容易理解,它就是用于提供数据,无论是在单个页面还是在整个app 都有它自己的解决方案,可以很方便的管理状态。
常用概念:
- ChangeNotifier:系统提供的被观察者,数据model需要继承
- Provider:订阅者,只用于数据共享管理,提供给子孙节点使用,UpdateShouldNotify Function,用于控制刷新时机
- ChangeNotifierProvider:订阅者,不仅能够提供数据供子孙节点使用,还可以在数据改变的时候通知所有消费者。 Model变化后会自动通知ChangeNotifierProvider(订阅者),ChangeNotifierProvider内部会重新构建InheritedWidget,而依赖该InheritedWidget的子孙Widget就会更新.
- MultiProvider:多个订阅者:实际上就是通过每一个provider都实现了的 cloneWithChild方法把自己一层一层包裹起来。
- Consumer:消费者,能够在复杂项目中,极大地缩小你的控件刷新范围。最多支持6中model
- Selector: 消费者,强化的Consumer,支持过滤刷新
使用流程:
- 添加依赖
- 创建数据 Model
- 创建顶层共享数据
- 顶层Provider包裹
- 在子页面中获取状态
Provider种类:
- Provider:只能提供恒定的数据,不能通知依赖它的子部件刷新
- ListenableProvider: 提供的对象是继承了 Listenable 抽象类的子类,必须实现其 addListener / removeListener 方法,通常不需要
- ChangeNotifierProvider: 对子节点提供一个继承/混入/实现了ChangeNotifier的类,只需要在Model中with ChangeNotifier ,然后在需要刷新状态时调用 notifyListeners 即可
- ValueListenableProvider: 提供实现了继承/混入/实现了ValueListenable的Model,实际上是专门用于处理只有一个单一变化数据的ChangeNotifier。
- StreamProvider: 专门用作提供(provide)一条 Single Stream。
- 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页面更新流程
Redux使用流程:
- 添加依赖
- 创建State
- 创建action
- 创建reducer
- 创建store
- 将Store放入顶层
- 在子页面中获取Store中的state
- 发出action
优点:
- 自动订阅
- 自动通知
- 可以定向通知
- 视图和业务逻辑分离
Redux 的缺点:
- Redux 核心仅仅关心数据管理,不关心具体什么场景来使用它,这是它的优点同时也是它的缺点.
- 在我们实际使用 Redux 中面临两个具体问题.
- Redux 的集中和 Component 的分治之间的矛盾.
- Redux 的 Reducer 需要一层层手动组装,带来的繁琐性和易错性.
Fish-Redux:
Fish Redux 的灵感主要来自于 Redux、React、Elm、Dva 这样的优秀框架,而 Fish Redux 站在巨人的肩膀上,将集中,分治,复用,隔离做的更进一步。
分层架构图
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总结:
优点:
- 数据集中管理,框架自动完成reducer合并。
- 组件分治管理,组件之间以及和容器之间互相隔离。
- View、Reducer、Effect隔离。易于编写复用。
- 声明式配置组装。
- 良好的扩展性。
最大的特点是配置式组装:
一方面将一个大的页面,对视图和数据层拆解为互相独立的 Component|Adapter,上层负责组装,下层负责实现;
另一方面将 Component|Adapter 拆分为 View、Reducer、Effect 等相互独立的上下文无关函数。
所以它会非常干净,易维护,易协作。
缺点:
- 框架设计比较重,适用于复杂的业务场景,加上复杂的目录结构以及相关概念,不太适合普通的数据不太复杂的业务。
- 学习成本比较高,虽然方案比较牛,各种支持,但是在实际使用中如果不能很好的使用,会影响开发效率。
8.状态管理总结&思考
如何选择框架?
没有哪一种框架可以适配所有的情况,也没有一种框架可以永远适用.
应该根据业务分析适合哪一种,当业务变化时,代码也需要跟着进化,以适配业务的发展.从一开始就介入fish_redux这样的框架,成本高,难度大,只是为了实现一些简单的二级,三级页面,并不是一个好的选择。
选型原则
- 侵入性
- 扩展性
- 高性能
- 安全性
- 驾驭性
- 易用性
- 范围性
所有的框架都有侵入性,你同意吗?
目前侵入性比较高的代表ScopedModel,如果你选择的框架只能使用它提供的几个入口,可以放弃使用它。
高性能:也是很重要的,这个需要明白它的原理,看它到底如何做的管理。
安全性:也很重要,看他数据管理通道是否安全稳定。
驾驭性:你说你都不理解你就敢用,出了问题找谁?如果驾驭不了也不要用。
易用性:大家应该都明白,如果用它一个框架需要N多配置,N多实现,放弃吧,不合适。简单才是硬道理。
范围性 :这个特点是flutter中比较明显的,框架选型一定要考虑框架的适用范围,到底是适合做局部管理,还是适合全局管理,要做一个实际的考量。
多种状态管理框架是否可以同时使用?
当然可以,你用了redux,就不允许setstate()了? 显然不是.
如何同时使用不同的框架能满足你的需求,使你的性能更好,使用更方便,可读性更强那就使用吧