Skip to content
Linbudu's Blog

【outdated】Flutter状态管理(一):使用Provider并复用你的Redux思想

1 min read

前言

个人认为, 状态管理真的是前端避不开的问题..., 随着应用复杂度的提升, 好的状态管理方案在解耦 & 数据共享 & 数据流追踪控制 等方面都能起到很好的作用. 在 Web 开发中, 我们使用过 Redux/Mobx/Reconciler 这些主流方案, 或者是基于其基本思想的 Dva/Icestore/Hox 等等. 在 Flutter 中进行状态管理, 这实际上也是我首次接触. 因此可能存在一些错误或是不足, 还请见谅.

要开始学习 Flutter 的状态管理, 我们务必需要了解到 Flutter 的声明式编程理念, 就像 JSX 一样的 UI = f(State) 思路, 我们通常把"状态"分为两类:

  • Ephemeral State, 瞬时状态

    这一类状态只会在其被定义的 widget 中使用, 不会发生状态共享, 也就是说其他 widget 不会有机会直接使用, 最多通过回调函数来更改.

  • Global State, 全局状态

    这一类状态是状态管理重点关注的部分, 它需要在全局被共享, 供多个 widget 读写, 如用户登录态与个性化配置等. 如果我们将这些数据每次都在各个 widget 间进行传递, 无疑会使得整体代码极度耦合, 维护起来更是想让你捶死之前的自己.

状态管理流程

实际上, 在编写 React 项目的思路与经验可以被大部分复用到 Flutter 项目中, 比如类似contextInheritedWidget, 类似Redux+React-Reduxprovider(需要额外安装的依赖).

Provider

由我翻译的 provider 中文文档

Provider 是官方推荐的状态管理方案, 我个人上手后感觉和Redux + React-Redux的体感类似, 并且非常容易上手, 它的底层同样基于[InheritedWidget], 官方给出的优势包括:

  • 对资源的简易配置与卸载
  • 懒加载
  • 减少模板代码
  • 开发者工具
  • 更友好的开发者工具

截至 2020.9.23, Provider 版本为4.3.2+2

在开始前, 我们可以尝试将其中的重要概念对标到React-Redux

  • ChangeNotifier: 数据存放的地方, 就像store
  • ChangeNotifierProvider: 提供数据的 Widget, 就像我们放在 React 组件最外层的<Provider>组件, ChangeNotifierProvider 只是提供的 providers 中最为常用的一种.
  • Consumer: 数据的消费者
  • Selector: 数据的清洗与'清洗过程优化', 就像useSelector或者是Reselect这种, 但暂时不清楚底层是否做了类似的缓存支持, 确定的是 Selector 会对集合类型的值做深比较

还是以计数器为例子:

首先创建一个ChangeNotifier, 保存状态:

在这里我使用的是 Provider 官方提供的例子, 混入DiagnosticableTreeMixin类并重写debugFillProperties方法主要是为了便于调试, 你也可以直接只继承ChangeNotifier

1import 'package:flutter/foundation.dart';
2import 'package:flutter/material.dart';
3import 'package:provider/provider.dart';
4
5class Counter with ChangeNotifier, DiagnosticableTreeMixin {
6 int _count = 0;
7 int get count => _count;
8
9 void increment() {
10 _count++;
11 notifyListeners();
12 }
13
14 void decrement() {
15 _count--;
16 notifyListeners();
17 }
18
19 @override
20 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
21 super.debugFillProperties(properties);
22 properties.add(IntProperty('count', count));
23 }
24}

我们在其中提供了两个方法来对_count进行修改, 并在修改完成后调用notifyListeners, 这里是为了通知所有该数据的 Consumer 进行更新.

接着, 提供ChangeNotifierProvider, 就像在 React 中那样, 我们需要把它放置到组件树的顶层:

1void main() {
2 runApp(MultiProvider(
3 providers: [
4 ChangeNotifierProvider(create: (_) => Counter()),
5 ],
6 child: StateManagementDemo(),
7 ));
8}
9
10class StateManagementDemo extends StatelessWidget {
11 const StateManagementDemo({Key key}) : super(key: key);
12
13 @override
14 Widget build(BuildContext context) {
15 return MaterialApp(
16 title: "Flutter 状态管理",
17 home: HomePage(),
18 );
19 }
20}

这里使用了MultiProvider, 来更清晰的组织状态树, 否则很可能出现 Provider 一层套一层的情况, 比如:

1Provider<Something>(
2 create: (_) => Something(),
3 child: Provider<SomethingElse>(
4 create: (_) => SomethingElse(),
5 child: Provider<AnotherThing>(
6 create: (_) => AnotherThing(),
7 child: someWidget,
8 ),
9 ),
10),

这样的思路我们在 Redux 中也经常使用.

现在 widget 树内就可以共享这些状态了, 但我们需要使用 Consumer 来进行构建:

1class HomePage extends StatelessWidget {
2 const HomePage({Key key}) : super(key: key);
3
4
5 Widget _text(BuildContext context, String text) {
6 return Text(text,
7 style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500));
8 }
9 @override
10 Widget build(BuildContext context) {
11 print("build");
12 return Center(
13 child: Scaffold(
14 appBar: AppBar(
15 title: const Text('Provider Example'),
16 ),
17 body: Center(
18 child: Column(
19 mainAxisAlignment: MainAxisAlignment.center,
20 children: <Widget>[
21 Text(
22 "Counter Cousumer",
23 style: Theme.of(context).textTheme.headline5,
24 ),
25 Consumer<Counter>(
26 builder: (ctx, counter, child) {
27 return Column(
28 children: <Widget>[
29 const Count(),
30 _text(context, "Consumer: ${counter.count}"),
31 ],
32 );
33 },
34 ),
35 Padding(padding: EdgeInsets.only(top: 20)),
36 Text(
37 "Transformed Counter Cousumer",
38 style: Theme.of(context).textTheme.headline6,
39 ),
40 Consumer<Transform>(
41 builder: (ctx, transform, child) {
42 return Column(
43 children: <Widget>[
44 _text(context,
45 "read: ${context.read<Transform>().transformed}"),
46 _text(context, 'Consumer: ${transform.transformed}'),
47 ],
48 );
49 },
50 ),
51 ],
52 ),
53 ),
54 floatingActionButton: // ...
55 ));
56 }
57}

我们重点看这一部分:

1Consumer<Counter>(
2 builder: (ctx, counter, child) {
3 return Column(
4 children: <Widget>[
5 const Count(),
6 _text(context, "Consumer: ${counter.count}"),
7 ],
8 );
9 },
10 ),

它接受三个参数:

  • ctx: BuildContext, 上下文(用于定位树中位置)
  • 当前ChangeNotifier对应的实例
  • child: 用于优化 widget rebuild 的手段, 使用方式见下面的例子

现在我们可以获取数据了, 那么消费呢? 我们创建floatingActionButton:

1floatingActionButton: Consumer<Counter>(
2 child: Icon(Icons.add),
3 builder: (ctx, counter, child) {
4 return FloatingActionButton(
5 child: child,
6 onPressed: () {
7 counter.increment();
8 });
9 })

像这样使用 child 参数来进行优化, 能够很好的控制 widget 的 rebuild.

这里使用了Selector来构建组件, Selector 的优势主要有:

  • 更简洁的数据转换写法, 它接收两个泛型, 即为转换前与转换后的数据类型
  • rebuild 控制, 这里实际上能拿到转换前后的实例, 因此你可以做细粒度的控制

这里的数据获取, 其实我们还有几种方式:

1// 1. 将Count抽离成单独的组件, 使用context.watch()来获取状态树, 并确保在Counter变化时rebuild HomePage组件, 这种将Consumer抽离成组件的方式能够起到优化性能的作用, 但不是必须的, 并且很少需要这么点性能提升.
2class Count extends StatelessWidget {
3 const Count({Key key}) : super(key: key);
4
5 @override
6 Widget build(BuildContext context) {
7 return _text(context,
8 'Extract Count to a separate widget & [context.watch]: ${context.watch<Counter>().count}');
9 }
10}
11
12// 2. 由于Provider基于InheritedWidget, 因此我们可以使用Provider.of, 但是实际上还是推荐Consumer, 因为毕竟自带性能优化(会确保尽可能少的rebuild)
13 _text(context,'Provider.of<counter>(ctx): ${Provider.of<Counter>(ctx).count}')

我们再来简单回顾一下:

  • 提供数据: 使用 ChangeNotifierProvider , 或是根据你的需求使用其他的 Provider 如FutureProvider等, 为了更好的组织状态树, 推荐使用
  • 消费数据: 使用 Consumer / Selector / Provider.of()(来自于InheritedWidget), 或者使用provider在构建上下文 context 上扩展的属性: watch/select/read(根据具体需求)
  • 性能优化: Selector 与context.select<T>()

另外一个可能比较常用的Provider: ProxyProvider, 它的用法主要是在数据层面对状态做转换, 可以同时接收多个 providers 的数据.

1void main() {
2 runApp(MultiProvider(
3 providers: [
4 ChangeNotifierProvider(
5 create: (_) => Counter(),
6 lazy: true,
7 ),
8 ProxyProvider<Counter, Transform>(
9 update: (_, counter, __) => Transform(counter.count))
10 ],
11 child: ProviderDemo(),
12 ));
13}
14
15class Transform {
16 final int _value;
17
18 const Transform(this._value);
19
20 String get transformed => "U clicked $_value times";
21}
22
23// build
24Text(
25 "Transformed Counter Cousumer",
26 style: Theme.of(context).textTheme.headline6,
27 ),
28 Consumer<Transform>(
29 builder: (ctx, transform, child) {
30 return Column(
31 children: <Widget>[
32 _text(context,
33 "read: ${context.read<Transform>().transformed}"),
34 _text(context, 'Consumer: ${transform.transformed}'),
35 ],
36 );
37 },
38 ),