Flutter|一文搞懂何谓状态管理

2022-09-20 00:00:00 数据 框架 状态 组件 管理

1.什么是状态管理

2.不同的状态管理分类

3.Flutter中的有状态组件和无状态组件

4.Flutter中有哪些可以做到状态管理

5.为什么要使用状态管理

6.常见的状态管理框架有哪些

7.状态管理总结&思考


01

什么是状态管理

随着“大前端”概念流行的同时,响应式编程的理念也随之被越来越多的人所了解和学习,要了解和学习响应式的编程框架就一定离不开状态管理,客户端Android、iOS是通过明确的命令式指令去控制我们的UI变化,如setText,而在响应式编程下,我们只需要描述好UI和状态之间的关系,然后专注于状态的改变就好了,框架会根据状态的变化来自动更新UI。

总结来说就是:状态管理就是当某个状态发生改变的时候,告知使用该状态的状态监听者,让状态所监听的属性随知改变,从而达到联动效果。

02

不同的状态管理分类

  • 短时状态Ephemeral state
    某些状态、或是可以理解为某些数据只需要在当前的Widget中访问和使用,不需要对这些状态进行共享访问,你需要的只是一个StatefulWidget组件,依靠这个StatefulWidget组件自己的State类自己管理即可,不需要使用状态管理框架去管理这种状态,这些状态可以称之为短时状态。
    如:官网中的计数器Demo、比如一个PageView组件记录当前的页面

  • 应用状态App state
    某些状态需要被组件共享访问,当这个状态发生变化的时候,其他组件也需要随之发生联动的变化,这就是应用状态。
    举个例子来说明,比如一个电商App,在商品的详情页面,我们把某个商品加入了购物车,那么商品是否放入购物车这个状态,就需要被购物车页面组件所访问,那么这个状态就是应用状态。
    试想一下,如果再不使用第三方状态管理框架的情况下,我们可以怎么实现呢,可以使用InheritedWidget定向的传递,可以通过Notification进行通知,可以使用event_bus来进行事件订阅等等,其实我们所说的状态管理框架,也是基于上面说的等几种方式来实现的。

    总结来说,要区分短时状态还是应用状态,就看这个状态需不需要被多个组件进行访问,当这个状态一发生变化,其他组件需要随之发生联动变化,就是应用状态,反之,其他组件不需要变化、不受影响,就是短时状态。


03

Flutter中的有状态组件和无状态组件

在Flutter中,组件根据状态分为,有状态组件StatefulWidget和无状态组件StatelessWidget。

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

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

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

State生命周期:


04

Flutter中有哪些可以做到状态管理

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使用。

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. 缺点恰恰是它的优点,保证了足够灵活,你更可基于它做一个好的设计,满足当下业务的设计。


05

为什么要使用状态管理

对于不需要传递的状态或者不需要共享的状态,我们不需要进行复杂的状态管理,单纯依靠setState也可以很好的完成我们的需求。

但是随着产品迭代节奏速度的加快,项目逐渐变得越来越庞大,不同组件之间的数据依赖性越来越高,我们就需要更清晰、明确的处理各个组件之间的数据关系,这时候如果还单单使用setState做状态处理,我们就很难明确的处理数据的流向,终可能会导致数据传递和嵌套逻辑过于复杂,不便于维护和管理,在出现问题的时候,也会花费大量的时间成本来捋清数据之间的关系。

总的来说,对于跨组件(跨页面)之间进行数据共享和传递,而且需要保持状态的一致性和可维护性,这就需要我们对状态进行管理。


06

常见的状态管理框架有哪些

Provider

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

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

  3. 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. 在子页面中获取状态

  • Provder种类:

  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推荐的状态管理方式。

  • 原理

  1. 所有的状态都存储在Store里。这个Store会放在根Widget.

  2. View拿到Store的状态数据会映射成视图渲染.

  3. Redux不直接让view操作数据,通过dispatch一个action通知Reducer,状态变更

  4. Reducer接收到这个action,根据action状态,生成新的状态,并替换在Store的旧状态.

  5. Store存储了新的状态后,就通知所有使用到了这个状态的View更新(类似setState)。这样我们就能够同步不同view中的状态了.

  • Redux相关概念

  1. State:数据model

  2. Store 仓库:整个APP的顶层,存储和管理state

  3. Action 动作:通过发起一个Action来告诉Reducer该更新状态了

  4. Reducer 还原:根据Action产生新的状态

  5. StoreProvider: 一个InheritedWidget,内部存储了一个Store。(数据中心)顶层必须是 StoreProvider 开始

  6. StoreConnector: 连接器:需要两个泛型
    1)一个是我们创建的 State(ReduxState)
    2)一个是 ViewModel,ViewModel决定了converter(转换函数)那边的返回值类型
    同时提供了一个StoreStreamListener,本质上是一个StreamBuilder

  7. StoreConverter:转换器:类似于Selector中的selector,转换成本Widget想要的数据

  8. StoreStreamListener: 通过监听自己的Stream来完成视图的重建。

  9. StoreBuilder:功能同StoreConnector,StoreConnector主要是有个数据转化的作用,可以对数据先做一些转化操作再赋值到组件上,StoreBuilder是直接将数据给显示在组件上

  10. middleware 中间件:类似拦截器,作用域位于reducer更新状态之前,本质上也是一个函数。
    比如当前是添加用户动作,但是我想在添加用户这操作的前面再做一步其他的动作(异步 action ,action 过滤,日志输出,异常报告等),这时候就可以使用中间件middleware,实现MiddlewareClass该类就行。

  11. 中间件的call方法中有个关键方法next(),大多数情况需要调用,否则中间件的链条断了,后面的中间件和Reducer就不执行了。

  12. Dispatcher:如何通知状态更新呢?通过store.dispatch

  • Redux页面刷新流程

  • Redux使用流程:

  1. 添加依赖

  2. 创建State

  3. 创建action

  4. 创建reducer

  5. 创建store

  6. 将Store放入顶层

  7. 在子页面中获取Store中的state

  8. 发出action

  • 优点:

  1. 自动订阅

  2. 自动通知

  3. 可以定向通知

  4. 视图和业务逻辑分离

  • Redux 的缺点:

  1. Redux 核心仅仅关心数据管理,不关心具体什么场景来使用它,这是它的优点同时也是它的缺点.

  2. 在我们实际使用 Redux 中面临两个具体问题.

  3. Redux 的集中和 Component 的分治之间的矛盾.

  4. Redux 的 Reducer 需要一层层手动组装,带来的繁琐性和易错性.

GetX

GetX是Flutter上的一个轻量且强大的解决方案,包括但不限于:

  1. 高效的状态管理。

  2. 便捷的路由管理。

  3. 丰富的Api。

  • GetX的三项基本原则:

  1. 性能:GetX专注于性能和小资源消耗,GetX打包后的apk占用大小和运行时的内存占用与其他状态管理插件不相上下。

  2. 效率:GetX的语法非常便捷,并保持了极高的性能,能极大缩短你的开发时长。

  3. 结构:GetX可以将界面、逻辑、依赖和路由完全解藕,用起来更清爽,逻辑更清晰,代码更容易维护。

  • GetX高效的状态管理:
    之所以说GetX是高效的状态管理,是因为他不需要堆叠大量的控制、管理代码(如Action、middleware、reducer、state),而且不具有侵入性,可以降低业务和视图间的耦合度。
    在使用上,使用GetX的响应式状态管理就像使用setState一样简单(其实本质就是setState),并且GetX可以做到局部刷新。

  • 使用GetX实现一个简单的登陆功能
    Demo逻辑:HomePage为主页面,事件跳转到登录页面,登录页面登录成功后关闭页面,主页面刷新Text文案内容。

class HomePage extends StatelessWidget {  //实例化的Controller  final LoginController logic = Get.put(LoginController(), tag: 'login');
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('LoginPage'), ), body: Center( child: GestureDetector( onTap: () { Get.to(LoginPage()); }, child: Container( width: 200, height: 200, color: Colors.blue, child: Center( child: Obx( () => Text(logic.loginStatus.value), ), ), ), ), ), ); }}

登录页面

class LoginPage extends StatelessWidget {  @override  Widget build(BuildContext context) {    //通过find方法,可以找到你已经实例化的Controller    final LoginController loginController = Get.find(tag: 'login');    return Scaffold(      appBar: AppBar(        title: Text('LoginPage'),      ),      body: Center(        child: GestureDetector(          onTap: () {            loginController.login();          },          child: Container(            width: 200,            height: 200,            color: Colors.green,            child: Center(child: Text('点我登录')),          ),        ),      ),    );  }}

controller控制器

class LoginController extends GetxController{  ///通过.obs将loginStatus标记为被观察者  ///loginStatus是RxString类型的,不是String类型  var loginStatus = '未登录'.obs;  login() => {    loginStatus.value = '已登录',    Get.back(),    // update()  };}
  • 通过Demo感受到GetX的优点

  1. 业务、视图解藕,业务逻辑可以放在Controller中进行处理。

  2. 代码简洁,无需创建大量的控制类。

  3. 局部刷新,当被观察的数据发生变化时,只有观察者部分会进行刷新,不会整个页面进行刷新。

  4. 相同的方法(如login),如果被观察的数据没有发生变化,则不会进行局部刷新。

  5. 从此告别StatefulWidget。

  6. 更简单的实现跨页面交互事件。


07

状态管理总结&思考

7.1 如何选择框架

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

7.2 选型原则

  • 侵入性

  • 扩展性

  • 高性能

  • 安全性

  • 驾驭性

  • 易用性

  • 范围性

所有的框架都有侵入性,你同意吗?

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

高性能:也是很重要的,这个需要明白它的原理,看它到底如何做的管理。
安全性:也很重要,看他数据管理通道是否安全稳定。
驾驭性:你说你都不理解你就敢用,出了问题找谁?如果驾驭不了也不要用。
易用性:大家应该都明白,如果用它一个框架需要N多配置,N多实现,放弃吧,不合适。简单才是硬道理。
范围性 :这个特点是flutter中比较明显的,框架选型一定要考虑框架的适用范围,到底是适合做局部管理,还是适合全局管理,要做一个实际的考量。

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

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


相关文章