简单的应用状态管理
一种简单的状态管理形式。
既然你已经了解了声明式 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 的当前状态并将新数据应用到其中。用这种方式很难避免错误。
在 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<CartModel>。如果你不指定泛型 (<CartModel>),provider 包将无法帮助你。provider 是基于类型的,没有类型,它就不知道你想要什么。
Consumer 组件唯一必需的参数是 builder。Builder 是一个函数,每当 ChangeNotifier 发生变化时就会被调用。(换句话说,当你调用模型中的 notifyListeners() 时,所有相应 Consumer 组件的 builder 方法都会被调用。)
Builder 在调用时带有三个参数。第一个是 context,你在每个 build 方法中都能得到它。
builder 函数的第二个参数是 ChangeNotifier 的实例。这正是我们最初请求的对象。你可以使用模型中的数据来定义 UI 在任何给定点应该看起来是什么样子。
第三个参数是 child,它是为了优化而存在的。如果你在 Consumer 下有一个庞大的组件子树,且当模型发生变化时该子树*不会*改变,你可以构建一次并通过 builder 获取它。
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<CartModel> 来实现,但这会造成浪费。我们会要求框架重建一个实际上不需要重建的组件。
对于这种情况,我们可以使用 Provider.of,并将 listen 参数设置为 false。
Provider.of<CartModel>(context, listen: false).removeAll();
在 build 方法中使用上述行,当 notifyListeners 被调用时,不会导致该组件重建。
整合所有概念
#你可以查看本文涵盖的示例。如果你想要更简单的内容,请看看当使用 provider 构建简单的计数器应用时是什么样子。
通过学习这些文章,你已经大大提升了创建基于状态的应用的能力。尝试自己使用 provider 构建一个应用,以掌握这些技能。