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

因此,我们至少有 5 个 Widget
的子类。其中许多需要访问“属于”其他地方的状态。例如,每个 MyListItem
需要能够将自己添加到购物车。它可能还想查看当前显示的项目是否已在购物车中。
这引出了我们的第一个问题:我们应该把购物车当前的状态放在哪里?
状态提升
#在 Flutter 中,将状态保存在使用它的小部件之上是有意义的。
为什么?在像 Flutter 这样的声明式框架中,如果你想改变 UI,你必须重新构建它。没有简单的方法可以实现 MyCart.updateWith(somethingNew)
。换句话说,很难通过调用方法从外部命令式地改变小部件。即使你能让它工作,你也会与框架作对,而不是让它帮助你。
// BAD: DO NOT DO THIS
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 的当前状态,并将新数据应用到其中。这种方式很难避免 Bug。
在 Flutter 中,每次内容更改时,你都会构建一个新的小部件。你不是使用 MyCart.updateWith(somethingNew)
(方法调用),而是使用 MyCart(contents)
(构造函数)。因为你只能在其父级的 build 方法中构建新的小部件,所以如果你想更改 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
小部件就会消失,并被新的小部件完全替换。

这就是我们所说的部件是不可变的。它们不会改变——它们会被替换。
现在我们知道购物车状态放在哪里了,让我们看看如何访问它。
访问状态
#当用户点击目录中的某个商品时,该商品会被添加到购物车。但是,由于购物车位于 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 有机制让小部件向其后代(换句话说,不仅仅是其子级,而是其下面的任何小部件)提供数据和服务。正如你对 Flutter 的期望,其中一切皆小部件™,这些机制只是特殊类型的小部件——InheritedWidget
、InheritedNotifier
、InheritedModel
等。我们不会在这里讨论这些,因为它们对于我们想要做的事情来说有点低级。
相反,我们将使用一个与底层小部件协同工作但易于使用的包。它叫做 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 中的任何更高级别的类。它很容易测试(你甚至不需要使用小部件测试)。例如,这是一个 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
是一个向其后代提供 ChangeNotifier
实例的小部件。它来自 provider
包。
我们已经知道将 ChangeNotifierProvider
放在哪里:放在需要访问它的小部件之上。在 CartModel
的情况下,这意味着在 MyCart
和 MyCatalog
之上的某个位置。
你不想把 ChangeNotifierProvider
放在不必要的高层(因为你不想污染作用域)。但在我们的例子中,唯一同时位于 MyCart
和 MyCatalog
之上的小部件是 MyApp
。
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: const MyApp(),
),
);
}
请注意,我们正在定义一个构建器,它创建 CartModel
的一个新实例。ChangeNotifierProvider
足够智能,除非绝对必要,否则不会重建 CartModel
。当不再需要该实例时,它还会自动调用 CartModel
上的 dispose()
。
如果您想提供多个类,可以使用 MultiProvider
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => CartModel()),
Provider(create: (context) => SomeOtherClass()),
],
child: const MyApp(),
),
);
}
Consumer
#现在,通过顶部的 ChangeNotifierProvider
声明,CartModel
已提供给我们的应用程序中的小部件,我们可以开始使用它了。
这是通过 Consumer
小部件完成的。
return Consumer<CartModel>(
builder: (context, cart, child) {
return Text('Total price: ${cart.totalPrice}');
},
);
我们必须指定要访问的模型的类型。在本例中,我们要访问 CartModel
,因此我们写 Consumer
。如果你不指定泛型 (
),provider
包将无法帮助你。provider
是基于类型的,如果没有类型,它就不知道你想要什么。
Consumer
小部件唯一必需的参数是构建器。构建器是一个函数,每当 ChangeNotifier
更改时就会调用它。(换句话说,当你在模型中调用 notifyListeners()
时,所有相应的 Consumer
小部件的所有构建器方法都会被调用。)
构建器被调用时带有三个参数。第一个是 context
,你在每个构建方法中也会得到它。
构建器函数的第二个参数是 ChangeNotifier
的实例。这正是我们一开始所要求的。你可以使用模型中的数据来定义 UI 在任何给定时间应该是什么样子。
第三个参数是 child
,它用于优化。如果您的 Consumer
下有一个庞大的小部件子树,并且在模型更改时它不会更改,您可以构建它一次并通过构建器获取它。
return Consumer<CartModel>(
builder: (context, cart, child) => Stack(
children: [
// Use SomeExpensiveWidget here, without rebuilding every time.
?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
按钮想要允许用户从购物车中删除所有内容。它不需要显示购物车的内容,它只需要调用 clear()
方法。
我们可以为此使用 Consumer
,但这会造成浪费。我们将要求框架重建一个不需要重建的小部件。
对于这种用例,我们可以使用 Provider.of
,并将 listen
参数设置为 false
。
Provider.of<CartModel>(context, listen: false).removeAll();
在构建方法中使用上述代码行时,当调用 notifyListeners
时,此小部件不会重新构建。
整合所有概念
#您可以查看本文涵盖的示例。如果您想要更简单的,请参阅使用 provider
构建的简单计数器应用程序是什么样的。
通过阅读这些文章,您创建基于状态的应用程序的能力得到了极大的提升。尝试自己用 provider
构建一个应用程序,以掌握这些技能。