简单的应用状态管理
现在你已经了解了声明式 UI 编程以及瞬时状态和应用状态之间的区别,你已准备好学习简单的应用状态管理。
在本页中,我们将使用 provider
包。如果你是 Flutter 新手,并且没有充分的理由选择其他方法(Redux、Rx、hooks 等),那么这可能是你应该开始使用的方法。provider
包易于理解,代码量不多。它还使用了适用于所有其他方法的概念。
话虽如此,如果你对其他响应式框架的状态管理有深厚的背景,你可以在选项页面上找到列出的包和教程。
我们的示例
#
为了说明,请考虑以下这个简单的应用。
该应用有两个独立的屏幕:商品目录和购物车(分别由 MyCatalog
和 MyCart
widget 表示)。它可能是一个购物应用,但你可以在一个简单的社交网络应用中想象相同的结构(将目录替换为“墙”,将购物车替换为“收藏夹”)。
商品目录屏幕包含一个自定义的应用栏(MyAppBar
)和一个包含许多列表项(MyListItems
)的滚动视图。
这是应用被可视化为 widget 树的样子。

所以我们至少有 5 个 Widget
的子类。它们中的许多需要访问“属于”其他地方的状态。例如,每个 MyListItem
需要能够将自己添加到购物车。它可能还想查看当前显示的商品是否已在购物车中。
这引出了我们的第一个问题:我们应该把购物车的当前状态放在哪里?
状态提升
#在 Flutter 中,将状态保存在使用它的 widget 之上是有意义的。
为什么?在像 Flutter 这样的声明式框架中,如果你想改变 UI,你必须重建它。没有简单的方法来执行 MyCart.updateWith(somethingNew)
。换句话说,通过在其上调用方法从外部命令式地改变一个 widget 是困难的。即使你能做到这一点,你也是在对抗框架而不是让它帮助你。
// BAD: DO NOT DO THIS
void myTapHandler() {
var cartWidget = somehowGetMyCartWidget();
cartWidget.updateWith(item);
}
即使你让上面的代码能够工作,你将不得不在 MyCart
widget 中处理以下问题
// 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 的当前状态,并将新数据应用到它。这样很难避免 bug。
在 Flutter 中,每当 widget 的内容发生变化时,你都会构造一个新的 widget。你使用 MyCart(contents)
(一个构造函数)而不是 MyCart.updateWith(somethingNew)
(一个方法调用)。因为你只能在 widget 的父级的 build 方法中构造新的 widget,如果你想改变 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(
// Just construct the UI once, using the current state of the cart.
// ···
);
}
在我们的示例中,contents
需要存在于 MyApp
中。每当它发生变化时,它会从上方重建 MyCart
(稍后会详细介绍)。因此,MyCart
不需要担心生命周期——它只是声明在任何给定 contents
下要显示的内容。当它发生变化时,旧的 MyCart
widget 消失并完全被新的替换。

这就是我们所说的 widget 是不可变的。它们不会改变——它们会被替换。
现在我们知道购物车的状态应该放在哪里了,接下来看看如何访问它。
访问状态
#当用户点击商品目录中的一个商品时,它会被添加到购物车。但是由于购物车位于 MyListItem
之上,我们如何做到这一点呢?
一个简单的选项是提供一个回调,当 MyListItem
被点击时可以调用它。Dart 的函数是一等对象,所以你可以以任何你想要的方式传递它们。因此,在 MyCatalog
内部,你可以定义以下内容
@override
Widget build(BuildContext context) {
return SomeWidget(
// Construct the widget, passing it a reference to the method above.
MyListItem(myTapCallback),
);
}
void myTapCallback(Item item) {
print('user tapped on $item');
}
这种方法还行,但对于需要从许多不同地方修改的应用状态,你将不得不传递大量的回调——这很快就会变得繁琐。
幸运的是,Flutter 有机制让 widget 向其后代(换句话说,不仅仅是它们的子级,而是它们下面的任何 widget)提供数据和服务。正如你对 Flutter 的期望,其中万物皆是 Widget™,这些机制只是特殊类型的 widget——InheritedWidget
、InheritedNotifier
、InheritedModel
等。我们在这里不讨论这些,因为它们对于我们正在尝试做的事情来说有点底层。
相反,我们将使用一个与底层 widget 协同工作但使用简单的包。它叫做 provider
。
在使用 provider
之前,别忘了在你的 pubspec.yaml
中添加它的依赖。
要将 provider
包添加为依赖项,请运行 flutter pub add
flutter pub add provider
现在你可以 import 'package:provider/provider.dart';
并开始构建了。
使用 provider
,你无需担心回调或 InheritedWidgets
。但你需要理解 3 个概念
- ChangeNotifier
- ChangeNotifierProvider
- Consumer
ChangeNotifier
#ChangeNotifier
是 Flutter SDK 中包含的一个简单类,它为其监听器提供更改通知。换句话说,如果某个东西是一个 ChangeNotifier
,你可以订阅它的更改。(对于熟悉该术语的人来说,它是一种 Observable 形式。)
在 provider
中,ChangeNotifier
是封装应用状态的一种方式。对于非常简单的应用,一个 ChangeNotifier
就足够了。在复杂的应用中,你会有多个模型,因此会有多个 ChangeNotifier
。(你根本不需要在 provider
中使用 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();
}
}
唯一特定于 ChangeNotifier
的代码是对 notifyListeners()
的调用。每当模型以可能改变应用 UI 的方式发生变化时,就调用此方法。CartModel
中的其他一切都是模型本身及其业务逻辑。
ChangeNotifier
是 flutter:foundation
的一部分,不依赖于 Flutter 中的任何高级类。它易于测试(你甚至不需要为其使用widget 测试)。例如,这是一个 CartModel
的简单单元测试
test('adding item increases total cost', () {
final cart = CartModel();
final startingPrice = cart.totalPrice;
var i = 0;
cart.addListener(() {
expect(cart.totalPrice, greaterThan(startingPrice));
i++;
});
cart.add(Item('Dash'));
expect(i, 1);
});
ChangeNotifierProvider
#ChangeNotifierProvider
是一个 widget,它向其后代提供 ChangeNotifier
的实例。它来自 provider
包。
我们已经知道 ChangeNotifierProvider
应该放在哪里:需要访问它的 widget 之上。对于 CartModel
的情况,这意味着在 MyCart
和 MyCatalog
的上方。
你不希望将 ChangeNotifierProvider
放置得比必要更高(因为你不希望污染作用域)。但在我们的例子中,唯一位于 MyCart
和 MyCatalog
之上的 widget 是 MyApp
。
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: const MyApp(),
),
);
}
请注意,我们正在定义一个创建 CartModel
新实例的 builder。ChangeNotifierProvider
足够智能,除非绝对必要,否则不会重建 CartModel
。当实例不再需要时,它还会自动在 CartModel
上调用 dispose()
。
如果你想提供多个类,你可以使用 MultiProvider
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => CartModel()),
Provider(create: (context) => SomeOtherClass()),
],
child: const MyApp(),
),
);
}
Consumer
#现在,通过顶部的 ChangeNotifierProvider
声明,CartModel
已提供给应用中的 widget,我们可以开始使用它了。
这通过 Consumer
widget 完成。
return Consumer<CartModel>(
builder: (context, cart, child) {
return Text('Total price: ${cart.totalPrice}');
},
);
我们必须指定要访问的模型的类型。在这种情况下,我们想要 CartModel
,所以我们写 Consumer<CartModel>
。如果你不指定泛型(<CartModel>
),provider
包将无法帮助你。provider
是基于类型的,如果没有类型,它就不知道你想要什么。
Consumer
widget 唯一必需的参数是 builder。Builder 是一个函数,每当 ChangeNotifier
发生变化时都会调用它。(换句话说,当你在模型中调用 notifyListeners()
时,所有相应 Consumer
widget 的所有 builder 方法都会被调用。)
builder 会被调用并传入三个参数。第一个是 context
,你也在每个 build 方法中得到它。
builder 函数的第二个参数是 ChangeNotifier
的实例。这正是我们最初想要的。你可以使用模型中的数据来定义 UI 在任何给定时间点的样子。
第三个参数是 child
,它是为了优化而存在的。如果你的 Consumer
下有一个大型的 widget 子树,当模型改变时它不会改变,你可以构造它一次并通过 builder 获取它。
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
widget 放置在 widget 树中尽可能深的位置。你不想仅仅因为某个细节改变而重建 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
按钮希望允许用户从购物车中移除所有内容。它不需要显示购物车的内容,它只需要调用 clear()
方法。
我们可以为此使用 Consumer<CartModel>
,但这会造成浪费。我们将要求框架重建一个不需要重建的 widget。
对于这种情况,我们可以使用 Provider.of
,并将 listen
参数设置为 false
。
Provider.of<CartModel>(context, listen: false).removeAll();
在 build 方法中使用上面这行代码不会导致此 widget 在调用 notifyListeners
时重建。
整合所有概念
#你可以查看本文涵盖的示例。如果你想要更简单的东西,请查看使用 provider
构建的简单计数器应用是什么样子。
通过阅读这些文章,你已大大提高了创建基于状态应用的能力。尝试自己使用 provider
构建一个应用,以掌握这些技能。