状态管理是什么
我们知道最基本的程序是什么
程序 = 算法 + 数据结构
数据是程序的中心。数据结构和算法两个概念间的逻辑关系贯穿整个程序世界,首先两者表现为不可分割的关系。flutter也是一个程序。由此得出
Flutter = 算法 + 数据结构
那状态管理是什么?我们也用公式表示一下:
Flutter状态管理 = 算法 + 数据结构 + UI绑定
来看一段代码例子:
1 | class ThemeBloc { |
- AppTheme 是数据结构
- changeTheTheme 是算法
- StreamBuilder 是绑定UI
这样一整套代码的逻辑就是我们所说的Flutter状态管理。算法就是我们如何管理,数据结构是数据状态,状态管理的本质还是如何通过合理的算法管理数据,如何取,如何接收等,最终展示在UI上,通过UI的变更来提现状态的管理逻辑。
为什么需要状态管理
状态管理可以实现组件通信、跨组件数据储存。原生的状态变更是通过具体的组件直接复制,如果页面全部变更,是不是需要每一个都设置一遍,而Flutter的变更就简单粗暴 ,setState搞定,它背后的逻辑是重新build整个页面,发现有变更,再将新的数据复制。其实原生开发与Flutter的本质的区别就是数据与视图完全分离,目前原生也出现了UI绑定框架。说明flutter的设计的先进性。这样设计的弊端是什么?
答案就是页面如何刷新才是Flutter的关键
,原生也面临这个问题,页面的重绘导致丢帧问题,为了效果更好,我们很多时候都选择局部刷新,原生已经很明确的告诉UI要刷新什么更新什么,而对于Flutter来说,这一点恨不清晰,虽然Flutter也做了类似虚拟Dom优化重绘逻辑,但这远远不够,如何合理的更新UI才是最主要的,这个时候一大堆的状态管理就出来了,当然状态管理也不是仅仅为了解决更新问题。我们再思考一个问题,如果我有一个widget A,我想在另外一个widget B中改变widget A的一个状态,或者从网络、数据库取到数据,然后刷新它。
一个糟糕的写法是,A widget的 state为全局的,当B widget 需要刷新的时候 直接调用 AState对象的 setState
方法.
这样写有几个问题:
1、违背了封装的概念,其他所有类都可以拿到A的state对象。
2、A和B强耦合了。
3、每次都是重绘整个widget A 性能变差。
4、不利于测试。
如何变好了,这就需要我们选择一种合适的状态管理方式。
状态管理的目标
其实我们做状态管理,不仅仅是因为它的特点,也是更好地架构。
- 代码层次分明,易维护,易阅读。
- 可扩展,可以动态替换UI而不影响算法逻辑。
- 安全可靠,保持数据的稳定伸缩
- 性能佳,局部刷新。
这不仅仅是状态管理的目的,也是我们做一款优秀应用的基础架构。
状态管理的基本分类
- 局部管理 官方也称Ephemeral State,意思是短暂的状态,这种状态根本不需要做全局处理,StatefulWidget处理即可完成。
- 全局管理 官方称App state ,即应用状态,非短暂状态,你要在应用程序的许多部分之间共享,以及希望在用户会话之间保持的状态,就是我们所说的应用程序状态(也称共享状态)。例如:
- 用户偏好
- 登录信息
- 购物车
- 新闻阅读状态
总之,任何Flutter应用程序中都有两种概念性的状态类型。临时状态可以使用State和setState来事项,并且通常是单个窗口小部件的本地状态。剩下的就是应用的状态。两种类型在任何Flutter应用程序中都有自己的位置,两者之间的划分取决于你自己的喜好和应用程序的复杂性。没有最好的管理方式,只有最合适的管理方式。
底层逻辑
底层逻辑主要讲,Flutter中目前有哪些可以做到状态管理,有什么缺点,适合做什么不适合做什么,完全明白底层逻辑才不会畏惧复杂的逻辑,即使逻辑复杂也能选择合理的方式去管理状态。
State
StatefulWidget、SteamBuilder状态管理方式。
InheritedWidget
专门负责Widget树种数据共享的功能型Widget,如Provider、scoped_model就是基于它开发
Notification
与InheritedWidget正好相反,InheritedWidget是从上往下传递数据,Notification是从下往上,但两者都是在自己的Widget树种传递,无法跨越树传递。
Stream
数据流 如Bloc、flutter_redux、fish_redux等也都基于它来做实现。
下面一一分析
State
State是我们常用而且使用最频繁的一个状态管理类,它必须结合StatefulWidget一起使用,StreamBuilder继承自StatefulWidget,同样使用setState来管理状态。
为什么用State管理状态,而不是Widget本身。Flutter设计时让Widget本身是不变的,类似固定的配置信息,那么就需要一个角色来控制它,State就出现了,但State的任何更改都会强制整个Widget重新构建,当然也可以覆盖必要的方法自己控制逻辑。
注意:setState是整个Widget重新构建(子Widget也会销毁重建),这个点也是为什么不推荐大量使用StatefulWidget的原因。如果页面足够复杂,就会导致严重的性能损耗。建议使用StreamBuilder,它原理上是State,但它做到了子Widget的局部刷新,不会导致整个页面的重建。
State的缺点
- 无法做到夸组件共享数据(这个夸是无关联的,如果是直接的父子关系,不算是跨组件)。一般我们将State的子类设置为私有,所以无法做到让别的组件调用State的setState函数来刷新。
- setState会成为维护的难点,因为到处都有。随着页面状态的增多,可能调用的setState的地方会越来越多,不能统一管理。
- 处理数据逻辑和视图混合在一起,违反代码设计原则。比如网络的数据取出来setState的UI上,这样编写代码,导致状态和UI耦合在一起,不利于测试和复用。
小结
反过来讲,State简单高效,当复杂到需要更好地管理的时候再重构,一个基本原则就是,状态是否需要跨组件使用,如果需要那就用别的方法管理状态而不是State管理。
InheritedWidget
InheritedWidget是一个无私的Widget,它可以把自己的状态数据,无私交给所有的子Widget,所有的子Widget可以无条件的继承它的状态。它的数据是只读的,子widget不能修改。
小结
缺点:
- 容易造成不必要的刷新。
- 不支持跨页面(router)的状态,不是同一个树的状态无法获取。
- 数据不可变,必须结合StatefulWidget、ChangeNotifier或者Steam使用。
比较适合在一个属性Widget中,抽象出公有状态,每一个子Widget或者孙Widget都可以获取该状态,我们可以通过手段控制rebuild的粒度来优化重绘逻辑,但它更适合从上往下传递,如果从下往上传递,如何做,下面解答。
Notification
它是flutter中跨层数据共享的一种机制,它不是widget,它提供了dispatch方法,来让我们沿着context对应的Element节点向上逐层发送通知。
小结:
缺点
- 不支持跨页面的状态,准确的说不支持NotificationListener同级或者父级Widget的状态通知
- 本身不支持刷新UI,需要结合State使用
- 如果结合State,会导致整个UI的重绘,效率低下不科学。
使用起来简单,如果页面复杂度高不推荐。
Stream
Stream其实是一个生产者消费者模型,一端负责生产,一端负责消费,而且是纯Dart的实现,跟Flutter没什么关系,扯上关系的就是StreamBuilder来构建一个Stream通道的Widget,像知名的rxdart、Bloc、flutter_redux全都用到了Stream的api。所以学习它才是我们掌握状态管理的关键。
小结
缺点
- api生涩,不好理解。
- 需要定制化,才能满足更复杂的场景。
- 没有自动dispose逻辑。
总结
以上所有的状态更新都离不开State的支持。
状态管理的使用原则
局部管理优于全局
这个原则来源于,Flutter的性能优化,局部刷新肯定比全局刷新要好很多,那么我们在管理状态的同时,也要考虑该状态到底是局部还是全局,从而编写正确的逻辑。
保持数据安全性
用_
私有化状态,因为当开发人员众多,当别人看到你的变量的时候,第一反应可能不是找你提供的方法,而是直接对变量操作,那就有可能出现想不到的后果,如果他只能调用你提供的方法,那他就要遵循你方法的逻辑,避免数据被处理错误。
考虑页面重新build带来的影响
很多时候页面的重建都会调用build函数,也就是说,在一个生命周期内,build函数是多次被调用的,所以你就要考虑数据的初始化或者刷新怎么样才能合理。