文章翻译自官方文档的👉状态管理。算是状态管理的入门资料了。
如果你对响应式框架 App 的状态管理已经很熟悉了,那么你可以跳过前几节,可以只看后面的状态管理方法列表,算是对状态管理框架的复习了。
在开发 Flutter 的过程中,你可能需要在 App 的不用页面共享应用的状态。有很多种方法可以做到这一点,也有很多问题可以进一步的思考片。
在本篇文章中,你可以学到在 App 中处理状态管理的基本知识点。
从理解声明式开始
如果你是从命令式的框架来学 Flutter 的,比如 Android、IOS 等等。那么你需要从一个新的角度来思考 App 的开发了。
许多想法可能都不适用于 Flutter,比如,当页面发生变化的时候,不需要你修改 UI,而是从头重新构建 UI。Flutter 的运行速度非常快,即使是在每一帧进行处理,都可以完全做到这一点。
Flutter 是声明式的,这就意味着 Flutter 的是应用当前状态的反应:
当你应用的状态发生改变时,比如用户设置了 switch 开关,意味着触发了 UI 的重绘。UI 会从头开始重绘,所以就没必要去改变 UI 本身了。在Flutter中,没有 widget.setText 这样的代码,全部都是重绘 widget。
可以在这篇文章中了解👉声明式 UI。
声明式 UI 有很多好处,最突出的是,任何状态的 UI 都仅仅指向自己的代码,什么状态对应着什么样的UI,这个关系非常明确。
声明式的编程可能不像命令式那么直观,所以这一节就先介绍了一下声明式。
区分临时状态和应用状态
这一节介绍了应用状态和临时状态,并且每一种状态如何管理
大体上来说,应用的状态就是应用在运行时存在于内存中的所有数据。包括应用的资源、Flutter 框架保存的关于UI、动画状态、纹理、字体等所有的变量。尽管这种定义是可以的,但是对应用的架构没啥用。
首先,你甚至不需要管理一些状态,比如纹理,Flutter 框架帮你做了这一些。所以对状态的定义应该是 不论何时,只要重新构建 UI 所需要的任何数据。其次,我们开发者所管理的状态 分为两类:临时状态和应用状态。
临时状态
临时状态是开发者可以巧妙地内置在一个单一的 Widget 中的状态,临时状态有时又叫 UI 状态或者 Local 状态。
👆上面的定义是一个模糊的抽象概念,这里有几个小例子:
PageView当前的Page- 动画当前的进度
BottomNavigationBar当前选中的 tab
Widget 树中的其他部分几乎不需要访问这种状态,不需要对它序列化,也不需要用复杂的方式修改它。
也就是说,对这种状态不需要状态管理(ScopedModel, Redux 等等)技术,仅仅需要一个 StatefulWidget。
下面的例子中,你会看到_MyHomepageState持有了 _index 字段,这个字段表示 BottomNavigationBar 当前选中的 tab。 _index 就是临时状态。
class MyHomepage extends StatefulWidget {
const MyHomepage({Key? key}) : super(key: key);
@override
_MyHomepageState createState() => _MyHomepageState();
}
class _MyHomepageState extends State<MyHomepage> {
int _index = 0;
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
currentIndex: _index,
onTap: (newIndex) {
setState(() {
_index = newIndex;
});
},
// ... items ...
);
}
}
这里,在 StatefulWidget 的 State 中使用 setState() + 成员变量的方式,完全够用了。应用的其他部分也不需要访问 _index。这个变量仅仅在 MyHomepage 内部改变,如果用户重新打开应用,你也会很自然的将 _index 重置为0。
应用状态
这种状态不是临时的,在应用内是共享的,在用户的会话内保存,有时也被称为共享状态。
下面是几个应用状态的例子:
- 用户首选项
- 登录信息
- 社交App的网络通知
- 电商App的购物车
- 新闻App中的新闻读的状态
对于管理这种状态,就需要好好对比利弊了。考虑应用的性质、应用的复杂性、团队的经验等等
模糊的规则
首先声明一点,开发者可与你使用 State + setState() 的方式来管理所有的状态,并且 Flutter 团队也确实使用这种方式来管理一些简单的 app ,比如我们 flutter create 创建的样板工程。
反过来也一样,比如,你可以在特定的上文中,让 bottom navigation 选中的 tab 是全局共享的,需要从外部类来修改它,也可以让它保持在整个会话周期内,等等,这种情况下 _index 就是应用状态了,不再是临时状态。
所以说,在临时状态和应用状态之间没有一个清晰的一刀切的规则,有时候,临时状态和应用状态可能会转化。比如随着应用的增长扩张,刚开始还是明确的临时状态的状态,可能就上升为了应用状态。
出于这个原因,我们可以不完全相信下面这张图,下面这张图是一般的状态划分:
总的来说,Flutter 中有两个类型的状态。服务于单独一 Widget 的状态是临时状态,可以用过 State + setState() 的方式实现管理。其他的都是 应用状态,这两种状态都有其价值和地位,并且它们的分界线在于 App 的复杂性和开发者的开发偏好。
简单的管理应用状态
前面介绍了声明式 UI、临时状态和应用状态,在此基础上,可以继续学习应用状态的管理。
下面,使用 provider 库 来实现应用状态的管理。如果您是刚开始开发 Flutter 的,并且也没有特殊的原因选择其他的方式,那么 Provider 是一个不错的开始。大多状态管理的概念是相通,为provider 也非常好理解,不需要太多的代码。学完 Provider 对理解其他的方式非常有帮助。
如果您有很好的背景知识,可以直接看后面的方式对比。
学习案例
App有两个单独的页面:目录页 MyCatalog 和购物车MyCart 页面。也可以想象社交 App 的通讯录和收藏夹。效果如下图:
目录页面包括:一个导航栏 (MyAppBar) 和一个滚动列表 (MyListItems)。
Widget 树如下图:
现在至少有了5个 Widget,这几个 Widget 大多可能需要访问并不属于自己的状态。比如,每一个 MyListItem 都要能够把自己添加购物车中,并且也需要看自己是否已经存在在购物车中了。
这就有个问题了:我们应该将购物车当前的状态放在哪里?
状态提升
在 Flutter 中,把状态放在使用它的 Widget 之上是非常有帮助的。
为什么?
在像 Flutter 这样的声明式的框架中,如果你想改变 UI,你想要去重新构建它,没有办法直接调用
MyCart.updateWith(somethingNew) 的这样操作。也就是说,我们不能做到从外部调用 Widget 的方法来进行修改。虽然我们可以想办法做到这一点,但是这与框架的设计是相悖的。我们从代码层解释一下:
// BAD: 千万不要这么做 找到 widget 调用widget的方法
void myTapHandler() {
var cartWidget = somehowGetMyCartWidget();
cartWidget.updateWith(item);
}
即使你写了上面的代码,你还必须在“MyCart”小部件中处理暴漏的方法:
// BAD: DO NOT DO THIS
Widget build(BuildContext context) {
return SomeWidget(
// The initial state of the cart.
);
}
void updateWith(Item item) {
// Somehow you need to change the UI from here.
}
你需要考虑 UI 当前状态是什么,而且还让 UI 应用新的数据。这样做很容易出现问题。
In Flutter, you construct a new widget every time its contents change. Instead of MyCart.updateWith(somethingNew) (a method call) you use MyCart(contents) (a constructor). Because you can only construct new widgets in the build methods of their parents, if you want to change contents, it needs to live in MyCart’s parent or above.
在Flutter中,只要内容改变就会构建新的 Widget,我们可以使用 MyCart(contents) 代替 MyCart.updateWith(somethingNew),构造方法代替方法调用。由于 Widget 是层序的,父组件构建子组件,所以想要通过构建来改变 MyCart 的显示,就需要把想要改变的 contents 放到 MyCart 的父节点或以上。
// GOOD
void myTapHandler(BuildContext context) {
var cartModel = somehowGetMyCartModel(context);
cartModel.add(item);
}
对于构建 MyCart 的 UI 来说,现在仅仅只有一个代码入口了。
// GOOD
Widget build(BuildContext context) {
var cartModel = somehowGetMyCartModel(context);
return SomeWidget(
// 使用状态构建 UI
// ···
);
}
在我们的例子中, contents 存活于 MyApp 中。只要修改了,就构建 MyCart 。MyCart 不需要担心生命周期,它只需要声明出,给定的 contents 显示什么样的 UI 就可以了。只要 contents 修改了,那么旧的 MyCart 消失,新的 MyCart 显示。
这就是我们常说的 Widget 是不可变的。直接使用新的代替旧的。
现在我们就知道了购物车的状态放在什么地方,我们下面在看 怎么访问它。
Accessing the state
只要用户点击了目录列表的 item,item就会添加到购物车。由于购物车在 MyListItem 的层次上,两者是隔离的。那么 怎么做到添加呢?
A simple option is to provide a callback that MyListItem can call when it is clicked. Dart’s functions are first class objects, so you can pass them around any way you want. So, inside MyCatalog you can define the following:
一个简单的方法是——回调。当 MyListItem 被点击了,MyListItem 就调用回调。在 Dart 中,Dart 的方法是头等类对象,可以在任意的地方传递。因此在 MyCatalog 可以像下面定义:
@override
Widget build(BuildContext context) {
return SomeWidget(
// 把方法传递给 MyListItem
MyListItem(myTapCallback),
);
}
void myTapCallback(Item item) {
print('user tapped on $item');
}
这样做是可以的,但是应用状态可能会被任意的节点修改,上面 回调的方式 就意味着传递大量的回调,尤其是层层的传递。 这样的做法 过时 了。
幸运的是,Flutter 提供了一种为子孙节点提供数据和服务的机制,不仅仅是子节点,只要是后代都可以。一切皆是 Widget ,这种机制也是 Widget——InheritedWidget、InheritedNotifier、InheritedModel 等等。这类 Widget 的学习和使用,可以看其文档。
我们重点不是它。
我们继续使用 provider 库,使用很简单,就像 Flutter 的提供的组件一样简单。在使用之前需要把 provider 添加到 pubspec.yaml 中。
name: my_name
description: Blah blah blah.
# ...
dependencies:
flutter:
sdk: flutter
provider: ^6.0.0
dev_dependencies:
# ...
现在就可以导包了:import 'package:provider/provider.dart'; ,可以使用了。
With provider, you don’t need to worry about callbacks or InheritedWidgets. But you do need to understand 3 concepts:
使用 provider 不用担心回调问题 和 InheritedWidgets。仅仅需要理解三个概念:
- ChangeNotifier
- ChangeNotifierProvider
- Consumer
ChangeNotifier
ChangeNotifier 是 Flutter 中一个简单的类,可以向它的监听者发通知。所以,只要一个组件或者服务是 ChangeNotifier ,你就可以订阅它的的改变,有点像被观察者的概念。
在 provider 中,ChangeNotifier 是封装应用状态的一种方式。对于许多简单的 App ,
可能只要一个 ChangeNotifier 就可以了。对于复杂的 App,可能需要几个复杂的事件源—— ChangeNotifiers。多个事件源的场景也很简单,不需要担心 ChangeNotifier 和 provider 嵌套问题。
We create a new class that extends it, like so:
在我们的例子中,我们想要管理 ChangeNotifier 中的状态。我们创建一个新类继承者 ChangeNotifier。
class CartModel extends ChangeNotifier {
/// Internal, private state of the cart.
final List<Item> _items = [];
/// An unmodifiable view of the items in the cart.
UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
/// The current total price of all items (assuming all items cost $42).
int get totalPrice => _items.length * 42;
/// Adds [item] to cart. This and [removeAll] are the only ways to modify the
/// cart from the outside.
void add(Item item) {
_items.add(item);
// This call tells the widgets that are listening to this model to rebuild.
notifyListeners();
}
/// Removes all items from the cart.
void removeAll() {
_items.clear();
// This call tells the widgets that are listening to this model to rebuild.
notifyListeners();
}
}
自定义类唯一泛化的代码是:对 notifyListeners() 的调用。只要事件源发生改变,这个改变可能改变 UI,就调用 notifyListeners() 方法。 CartModel 的代码只是业务逻辑和模型本身。
ChangeNotifier 是 flutter:foundation 的一部分,不依赖更高层级的类,也很容易做单元测试,下面就是单元测试的例子。
test('adding item increases total cost', () {
final cart = CartModel();
final startingPrice = cart.totalPrice;
cart.addListener(() {
expect(cart.totalPrice, greaterThan(startingPrice));
});
cart.add(Item('Dash'));
});
ChangeNotifierProvider
ChangeNotifierProvider is the widget that provides an instance of a ChangeNotifier to its descendants. It comes from the provider package.
ChangeNotifierProvider 是 provider 包中的 widget,为子孙节点提供一个 ChangeNotifier 实例。
我们已经知道了把 ChangeNotifierProvider 放在哪里:需要访问实例的 widget 的层级之上。
在这个例子中,CartModel 需要放置在 MyCart 和 MyCatalog 之上。
You don’t want to place ChangeNotifierProvider higher than necessary (because you don’t want to pollute the scope). But in our case, the only widget that is on top of both MyCart and MyCatalog is MyApp.
你可能不想把 ChangeNotifierProvider 放置的比必要的层序还要高,比如不想污染 scope。但是在我们的例子中,比 MyCart 和 MyCatalog 层序都高的是 MyApp。
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: const MyApp(),
),
);
}
注意我们定义了 builder 来构建 CartModel 实例, ChangeNotifierProvider 是非常
聪明的,它只会在必要的时候重构 CartModel 。也会在实例不在需要的时候自动调用 dispose() 方法。
如果你的事件源超过一个,可以使用 MultiProvider:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => CartModel()),
Provider(create: (context) => SomeOtherClass()),
],
child: const MyApp(),
),
);
}
Consumer
现在我们通过 ChangeNotifierProvider 为子孙节点提供了 CartModel ,我们可以使用它了。
可以使用 Consumer widget 来完成这一步。
return Consumer<CartModel>(
builder: (context, cart, child) {
return Text("Total price: ${cart.totalPrice}");
},
);
我们必须指定我们想要访问的类型,在这个例子中,我们想要 CartModel,所以我们写了 Consumer<CartModel>。如果你不指定泛型, provider 就不起作用了。 provider 的设计是基于类型的设计,如果不指定类型,那么它就不知道你想要啥了。
Consumer widget 唯一的必须参数是一个 builder,builder 是一个方法。只要 ChangeNotifier 发生了改变,那么 builder 方法就会被调用。所以只要 model 的 notifyListeners() 方法被调用了,所有相对应的 Consumer 的 builder 方法都会被调用。
builder 方法有三个参数,第一个是 context。第二个是 ChangeNotifier 的实例,这个实例中一般会包含 UI 想要的数据。第三个参数是 child,这个参数是可选的。应用场景是 Consumer 的有很大一块子树不需要响应数据,那么可以通过 child 只构建一次,后面的构建都是靠传递。
return Consumer<CartModel>(
builder: (context, cart, child) => Stack(
children: [
// Use SomeExpensiveWidget here, without rebuilding every time.
if (child != null) child,
Text("Total price: ${cart.totalPrice}"),
],
),
// Build the expensive widget here.
child: const SomeExpensiveWidget(),
);
最佳实践是尽可能把 Consumer 放置的层序足够深。一般情况下,你会仅仅因为某个地方的某个细节发生了改变,就重新构建大部分 UI。
// DON'T DO THIS
return Consumer<CartModel>(
builder: (context, cart, child) {
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Text('Total price: ${cart.totalPrice}'),
),
);
},
);
使用下面的方式:
// DO THIS
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Consumer<CartModel>(
builder: (context, cart, child) {
return Text('Total price: ${cart.totalPrice}');
},
),
),
);
Provider.of
有些时候,你可能并不是真正的需要数据来更改UI,只是单纯的访问它。比如说 ClearCart 按钮想要允许用户移除所有的商品。那么你不必显示购物车的 UI,只需要调用 clear() 方法。
我们可能使用 Consumer<CartModel> ,但是这可能是浪费的,因为我们要求 framework 去重新构建了一个需要重构的 UI —— ClearCart 按钮。
对于这种情况,我们可以使用 Provider.of,并且将 listen 设置为 false。
Provider.of<CartModel>(context, listen: false).removeAll();
Using the above line in a build method won’t cause this widget to rebuild when notifyListeners is called.
上面的代码的作用是,当 notifyListeners 被调用时,widget 不会重新构建。
把这些结合起来
可以从👉check out the example 看到文章的代码。如果想要简单了解的话,可以看 👉built with provider 代码,这个代码是对计数器代码的改造。
通过这一节,我们对应用状态的管理有了很好的了解,下面可以使用 provider 来构建自己的 App 了。
其他的状态管理框架
状态管理是一个复杂的话题,假如有一些问题还没回答清楚,或者上面介绍的方法不可用,你可以看看下面的内容。
在下面的链接中了解更多,其中许多已经由Flutter社区贡献:
方法总览
在选择一中方式的时候,先考虑一下内容:
- Pragmatic State Management in Flutter, Google I/O 2019 视频
- Flutter Architecture Samples, 作者 Brian Egan
Provider
推荐的使用方式
- Provider package
- You might not need Redux: The Flutter edition, 作者 Ryan Edge
- Making sense of all those Flutter Providers
Riverpod
Riverpod, another good choice, is similar to Provider and is compile-safe and testable. Riverpod doesn’t have a dependency on the Flutter SDK.
Riverpod是另一个不错的选择,它类似于Provider,是编译安全且可单元测试。并且Riverpod没有对Flutter SDK的依赖。
setState
临时状态的管理方式
- Adding interactivity to your Flutter app, Flutter 文档
- Basic state management in Google Flutter, 作者 Agung Surya
InheritedWidget & InheritedModel
祖先节点和子孙节点通信的方式,provider 以及很多三方库都是基于这一机制。
下面是 InheritedWidget 使用的介绍:
- InheritedWidget docs
- Managing Flutter Application State With InheritedWidgets, 作者 Hans Muller
- Inheriting Widgets, 作者 Mehmet Fidanboylu
- Using Flutter Inherited Widgets Effectively, 作者 Eric Windmill
- Widget - State - Context - InheritedWidget, 作者 Didier Bolelens
Redux
许多web开发人员都熟悉的状态容器方式
- Animation Management with Redux and Flutter, DartConf 2018视频 Accompanying article on Medium
- Flutter Redux package
- Redux Saga Middleware Dart and Flutter, 作者 Bilal Uslu
- Introduction to Redux in Flutter, 作者 Xavi Rigau
- Flutter + Redux—How to make a shopping list app, 作者 Paulina Szklarska on Hackernoon
- Building a TODO application (CRUD) in Flutter with Redux—Part 1, Tensor Programming的视频
- Flutter Redux Thunk, an example, 作者 Jack Wong
- Building a (large) Flutter app with Redux, 作者 Hillel Coren
- Fish-Redux–An assembled flutter application framework based on Redux, 作者 Alibaba
- Async Redux–Redux without boilerplate. Allows for both sync and async reducers, 作者 Marcelo Glasberg
- Flutter meets Redux: The Redux way of managing Flutter applications state, 作者 Amir Ghezelbash
- Redux and epics for better-organized code in Flutter apps, 作者 Nihad Delic
Fish-Redux
基于 Redux 状态管理的 Flutter 应用框架,适用于构建中型和大型应用程序。
- Fish-Redux-Library package, 作者 Alibaba
- Fish-Redux-Source, 源码
- Flutter-Movie, 演示用例
BLoC / Rx
基于流和观察者模式的方式
- Architect your Flutter project using BLoC pattern, 作者 Sagar Suri
- BloC Library, 作者 Felix Angelov
- Reactive Programming - Streams - BLoC - Practical Use Cases, 作者 Didier Boelens
GetIt
基于服务定位的状态管理方法,不需要 BuildContext。
- GetIt package,服务定位器可以和BloCs结合使用.
- GetIt Mixin package,
GetIt的混合,提供了状态管理方案 - GetIt Hooks package,
- Flutter state management for minimalists, 作者 Suragch
MobX
基于观察者和响应式的库
- MobX.dart, Hassle free state-management for your Dart and Flutter apps
- Getting started with MobX.dart
- Flutter: State Management with Mobx, 作者 Paul Halliday
Flutter Commands
使用命令模式的响应式状态管理,最好与 GetIt 结合使用,但也可以与 Provider 或其他定位器一起使用。
- Flutter Command package
- RxCommand package,
Stream的实现方式
Binder
基于 InheritedWidget 的状态管理. 关注于 concerns 的分离.
- Binder package
- Binder examples
- Binder snippets, vscode 的代码段
GetX
简单的状态管理方案
- GetX package
- Complete GetX State Management, 作者 Tadas Petra
- GetX Flutter Firebase Auth Example, 作者 Jeff McMorris
states_rebuilder
结合了状态管理、依赖注入和路由。
Triple Pattern (Segmented State Pattern)
使用 Streams 或 ValueNotifier 的状态管理模式。这种机制(称为triple的原因:流总是使用三个值: Error, Loading 和 State)基于Segmented状态模式。