— 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 项目中, 比如类似context
的InheritedWidget
, 类似Redux
+React-Redux
的provider
(需要额外安装的依赖).
由我翻译的 provider 中文文档
Provider 是官方推荐的状态管理方案, 我个人上手后感觉和Redux
+ React-Redux
的体感类似, 并且非常容易上手, 它的底层同样基于[InheritedWidget]
, 官方给出的优势包括:
截至 2020.9.23, Provider 版本为
4.3.2+2
在开始前, 我们可以尝试将其中的重要概念对标到React-Redux
中
store
<Provider>
组件, ChangeNotifierProvider 只是提供的 providers 中最为常用的一种.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 @override20 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 @override14 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 @override10 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 ),
它接受三个参数:
BuildContext
, 上下文(用于定位树中位置)ChangeNotifier
对应的实例现在我们可以获取数据了, 那么消费呢? 我们创建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 的优势主要有:
这里的数据获取, 其实我们还有几种方式:
1// 1. 将Count抽离成单独的组件, 使用context.watch()来获取状态树, 并确保在Counter变化时rebuild HomePage组件, 这种将Consumer抽离成组件的方式能够起到优化性能的作用, 但不是必须的, 并且很少需要这么点性能提升.2class Count extends StatelessWidget {3 const Count({Key key}) : super(key: key);4
5 @override6 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}')
我们再来简单回顾一下:
FutureProvider
等, 为了更好的组织状态树, 推荐使用Consumer
/ Selector
/ Provider.of()
(来自于InheritedWidget
), 或者使用provider
在构建上下文 context 上扩展的属性: watch
/select
/read
(根据具体需求)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// build24Text(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 ),