状态管理

161 阅读14分钟

0.前言

响应式的编程框架中都会有一个永恒的主题——“状态(State)管理”,在响应式编程下,我们只需要描述好UI和状态之间的关系,然后专注于状态的改变就好了,框架会根据状态的变化来自动更新UI。

在Flutter中,状态管理是十分重要的,因为Flutter的UI是声明式的,即UI是由当前状态决定的,而不是通过修改UI来改变状态,这就要求我们必须清晰地掌握和管理应用的状态,以确保UI的正确渲染和更新。良好的状态管理可以使开发者更高效地开发和维护代码。

1.什么是状态管理

在软件开发中,

  • 状态是指程序或应用在运行过程中的各种数据和状态信息,包括用户输入、网络请求、页面展示等等。
  • 状态管理是指在应用中对这些状态进行管理的过程。

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

image.png

2.状态分类

需要我们自己 管理 的状态可以分为两种概念类型:短时 (ephemeral) 状态和应用 (app) 状态。

  • 2.1 短时状态(Ephemeral state)

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

  • 2.2 应用状态(App state)

某些状态需要被组件共享访问,当这个状态发生变化的时候,其他组件也需要随之发生联动的变化,这就是应用状态。
举个例子来说明,比如一个电商App,在商品的详情页面,我们把某个商品加入了购物车,那么商品是否放入购物车这个状态,就需要被购物车页面组件所访问,那么这个状态就是应用状态。

image.png

试想一下,如果在不使用第三方状态管理框架的情况下,我们可以怎么实现呢,可以使用InheritedWidget定向的传递,可以通过Notification进行通知,可以使用event_bus来进行事件订阅等等,其实我们所说的状态管理框架,也是基于上面说的等几种方式来实现的。

3.管理状态的最常见的方法

思考一个问题:StatefulWidget的状态应该被谁管理?Widget本身?父 Widget ?都会?还是另一个对象?

答案是取决于实际情况!以下是管理状态的最常见的方法:

  1. Widget 管理自己的状态。
  2. 父 Widget 管理子 Widget 状态。
  3. 混合管理(父 Widget 和子 Widget 都管理状态)。

如何决定使用哪种管理方法?下面是官方给出的一些原则可以帮助你做决定:

  • 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父 Widget 管理。
  • 如果状态是有关界面外观效果的,例如颜色、动画,那么状态最好由 Widget 本身来管理。
  • 如果某一个状态是不同 Widget 共享的则最好由它们共同的父 Widget 管理。

在 Widget 内部管理状态封装性会好一些,而在父 Widget 中管理会比较灵活。有些时候,如果不确定到底该怎么管理状态,那么推荐的首选是在父 Widget 中管理(灵活会显得更重要一些)。

我们将通过创建三个简单示例TapboxA、TapboxB和TapboxC来说明管理状态的不同方式。 这些例子功能是相似的:创建一个盒子,当点击它时,盒子背景会在绿色与灰色之间切换。状态 _active确定颜色:绿色为true ,灰色为false。 如图:image.png

3.1 Widget 管理自己的状态

示例:TapboxA 管理自身状态

/// TapboxA 管理自身状态.

//------------------------- TapboxA ----------------------------------
class TapboxA extends StatefulWidget {
  TapboxA({Key? key}) : super(key: key);

  @override
  _TapboxAState createState() => _TapboxAState();
}

/// 对应的_TapboxAState 类
///    - 管理TapboxA的状态。
///    - 定义`_active`:确定盒子的当前颜色的布尔值。
///    - 定义`_handleTap()`函数,该函数在点击该盒子时更新`_active`,并调用`setState()`更新UI。
    - 实现widget的所有交互式行为。
class _TapboxAState extends State<TapboxA> {
  bool _active = false;

  void _handleTap() {
    setState(() {
      _active = !_active;
    });
  }

  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        child: Center(
          child: Text(
            _active ? 'Active' : 'Inactive',
            style: TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: _active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}

3.2 父 Widget 管理子 Widget 状态

子Widget通过回调将其状态导出到其父Widget,状态由父Widget管理。

示例:

// ParentWidget 为 TapboxB 管理状态.

//------------------------ ParentWidget --------------------------------

class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

/// ParentWidgetState 类:
///    - 为TapboxB 管理_active状态。
///    - 实现_handleTapboxChanged(),当盒子被点击时调用的方法。
///    - 当状态改变时,调用setState()更新UI。
class _ParentWidgetState extends State<ParentWidget> {
  bool _active = false;

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: TapboxB(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}

//------------------------- TapboxB ----------------------------------

/// TapboxB 类:
///    - 继承`StatelessWidget`类,因为所有状态都由其父组件处理。
///    - 当检测到点击时,它会通知父组件。
class TapboxB extends StatelessWidget {
  TapboxB({Key? key, this.active: false, required this.onChanged})
      : super(key: key);

  final bool active;
  final ValueChanged<bool> onChanged;

  void _handleTap() {
    onChanged(!active);
  }

  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        child: Center(
          child: Text(
            active ? 'Active' : 'Inactive',
            style: TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}

3.3 混合状态管理

子Widget自身管理一些内部状态,而父Widget管理一些其他外部状态。

示例:当手指按下时,盒子的周围会出现一个深绿色的边框,抬起时,边框消失。点击完成后,盒子的颜色改变。 TapboxC 将其_active状态导出到其父组件中,但在内部管理其_highlight状态。这个例子有两个状态对象_ParentWidgetState_TapboxCState

//---------------------------- ParentWidget ----------------------------

class ParentWidgetC extends StatefulWidget {
  @override
  _ParentWidgetCState createState() => _ParentWidgetCState();
}

/// _ParentWidgetStateC类:
///    - 管理`_active` 状态。
///    - 实现 `_handleTapboxChanged()` ,当盒子被点击时调用。
///    - 当点击盒子并且`_active`状态改变时调用`setState()`更新UI。
class _ParentWidgetCState extends State<ParentWidgetC> {
  bool _active = false;

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: TapboxC(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}

//----------------------------- TapboxC ------------------------------

class TapboxC extends StatefulWidget {
  TapboxC({Key? key, this.active: false, required this.onChanged})
      : super(key: key);

  final bool active;
  final ValueChanged<bool> onChanged;
  
  @override
  _TapboxCState createState() => _TapboxCState();
}

/// `_TapboxCState` 对象:
///    - 管理`_highlight` 状态。
///    - `GestureDetector`监听所有tap事件。当用户点下时,它添加高亮(深绿色边框);当用户释放时,会移除高亮。
///    - 当按下、抬起、或者取消点击时更新`_highlight`状态,调用`setState()`更新UI。
///    - 当点击时,将状态的改变传递给父组件。
class _TapboxCState extends State<TapboxC> {
  bool _highlight = false;

  void _handleTapDown(TapDownDetails details) {
    setState(() {
      _highlight = true;
    });
  }

  void _handleTapUp(TapUpDetails details) {
    setState(() {
      _highlight = false;
    });
  }

  void _handleTapCancel() {
    setState(() {
      _highlight = false;
    });
  }

  void _handleTap() {
    widget.onChanged(!widget.active);
  }

  @override
  Widget build(BuildContext context) {
    // 在按下时添加绿色边框,当抬起时,取消高亮  
    return GestureDetector(
      onTapDown: _handleTapDown, // 处理按下事件
      onTapUp: _handleTapUp, // 处理抬起事件
      onTap: _handleTap,
      onTapCancel: _handleTapCancel,
      child: Container(
        child: Center(
          child: Text(
            widget.active ? 'Active' : 'Inactive',
            style: TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: widget.active ? Colors.lightGreen[700] : Colors.grey[600],
          border: _highlight
              ? Border.all(
                  color: Colors.teal[700],
                  width: 10.0,
                )
              : null,
        ),
      ),
    );
  }
}

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

4.1 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的局部刷新,不会导致整个页面的重建。

4.2 InheritedWidget

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

InheritedWidget缺点:

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

4.3 Notification

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

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

4.4 Stream

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

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

5.常见的状态管理框架

5.1 Provider

Provider 是一个轻量级的状态管理框架,可用于单个 Widget 或整个 Widget 树中分发状态。它通过 InheritedWidget 和 ChangeNotifier 来实现状态管理,并支持依赖项注入。

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

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这样才能消除掉依赖关系。

5.2 Redux

Redux 库是将状态和业务逻辑从 UI 中清晰分离的一种方式。它通过一个单一的状态存储库来管理应用程序的状态,并使用可预测的方式修改状态。

5.3 MobX

MobX 是一种基于响应式编程的状态管理框架,它使用观察者模式来观察和响应状态的变化,并可以自动地更新 UI。

5.4 BLoC

BLoC 是一种基于 Reactive Programming 和 Stream 的状态管理模式,它将应用程序的状态分为三层:Business Logic、View 和 UI。Business Logic 层负责逻辑处理,View 层负责渲染,UI 层则负责响应用户的操作。

5.5 GetX

GetX 是一个轻量级的状态管理框架,提供了路由、依赖注入、状态管理等功能,通过依赖注入和静态扩展,让您能更加方便地构建结构清晰、易于维护的架构。

5.6 优缺点对比

框架优点缺点
Provider简单易用,轻量级;支持依赖项注入;方便快捷的状态管理。难以处理大型应用中的复杂状态;不支持异步操作;共享状态跨 widget 树。
Redux独立的状态管理,方便统一和管理;可预测且容易测试;支持中间件,方便处理异步操作。学习成本较高;可能存在大量的样板代码;对于小型应用过于复杂。
MobX响应式编程,易于理解和使用;自动化生成代码,方便快捷;扩展性很强。状态分散,可能难以掌握应用的状态流;可能存在过多的注释和无用代码;需要加注解,使代码变得繁琐。
BLoC适用于大型应用;规范的模式,方便维护;支持异步操作。增加了代码复杂度;学习成本略高。
GetX简单易用,轻便;提供完整的路由、依赖注入等功能;支持响应式编程。在大型应用中,可能会难以管理依赖关系;响应式编程可能导致性能问题。

6.回顾总结

  • 数据共享和同步:在应用程序中,不同部分可能需要共享和同步数据。通过状态管理,可以轻松地在应用程序的各个部分之间共享数据,并确保数据的一致性。
  • UI更新:Flutter状态管理可以帮助开发者管理应用程序中的UI状态,以便在数据变化时更新用户界面。这样可以确保应用程序的UI与数据的状态保持同步。
  • 复杂状态管理:随着应用程序变得越来越复杂,管理应用程序的状态变得更加困难。Flutter状态管理工具可以帮助开发者更有效地管理应用程序的状态,使代码更具可维护性和可扩展性。
  • 性能优化:有效的状态管理可以帮助应用程序避免不必要的重绘和重新构建,从而提高应用程序的性能和响应速度。
  • 代码结构:通过良好的状态管理,开发者可以更好地组织应用程序的代码结构,使其更易于理解和维护。