简单的应用状态管理
现在您已经了解了 声明式 UI 编程 以及 临时状态与应用状态 之间的区别,您可以准备了解简单的应用状态管理。
在此页面中,我们将使用 provider
包。如果您是 Flutter 新手,并且没有充分的理由选择其他方法(Redux、Rx、钩子等),那么这可能是您应该开始使用的方法。 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)
(一个构造函数)。因为你只能在父微件的构建方法中构造新微件,所以如果你想更改 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
。在复杂应用中,你将有几个模型,因此有几个 ChangeNotifiers
。(你根本不需要将 ChangeNotifier
与 provider
一起使用,但它是一个易于使用的类。)
在我们的购物应用示例中,我们希望在 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
现在,CartModel
通过顶部的 ChangeNotifierProvider
声明提供给应用中的小部件,我们可以开始使用它了。
这是通过 Consumer
组件完成的。
return Consumer<CartModel>(
builder: (context, cart, child) {
return Text('Total price: ${cart.totalPrice}');
},
);
我们必须指定要访问的模型类型。在本例中,我们希望 CartModel
,因此我们编写 Consumer<CartModel>
。如果您不指定泛型 (<CartModel>
),则 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.
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
按钮希望允许用户从购物车中删除所有内容。它不需要显示购物车的内容,它只需要调用 clear()
方法即可。
我们可以为此使用 Consumer<CartModel>
,但这会造成浪费。我们会要求框架重新构建一个不需要重新构建的小组件。
对于此用例,我们可以使用 Provider.of
,其中 listen
参数设置为 false
。
Provider.of<CartModel>(context, listen: false).removeAll();
在构建方法中使用上述代码行不会导致在调用 notifyListeners
时重新构建此小组件。
将所有内容放在一起
你可以查看本文中介绍的示例。如果你想要更简单的示例,请参阅使用 provider
构建的简单计数器应用。
通过遵循这些文章,你极大地提高了创建基于状态的应用程序的能力。尝试使用 provider
自己构建一个应用程序以掌握这些技能。