简单应用状态管理
现在您已经了解了声明式 UI 编程以及短暂状态和应用状态之间的区别,您就可以学习简单的应用状态管理了。
在本页中,我们将使用provider
包。如果您是 Flutter 新手,并且没有充分理由选择其他方法(Redux、Rx、hooks 等),那么这可能是您应该首先尝试的方法。provider
包易于理解,而且代码量不多。它还使用了适用于其他所有方法的概念。
也就是说,如果您在其他反应式框架中拥有丰富的状态管理经验,您可以在选项页面上找到列出的包和教程。
示例
#![一个动画gif显示一个Flutter应用程序的使用情况。它从用户登录屏幕开始。他们登录并进入产品目录屏幕,其中列出了商品。他们点击几件商品,当他们这样做时,这些商品会被标记为“已添加”。用户点击一个按钮,进入购物车视图。他们在那里看到商品。他们返回产品目录,他们购买的商品仍然显示为“已添加”。动画结束。](/assets/images/docs/development/data-and-backend/state-mgmt/model-shopper-screencast.gif)
为了说明,请考虑以下简单的应用程序。
该应用程序有两个单独的屏幕:产品目录和购物车(分别由MyCatalog
和MyCart
小部件表示)。它可以是一个购物应用程序,但您可以在简单的社交网络应用程序中想象相同的结构(将产品目录替换为“墙”,将购物车替换为“收藏夹”)。
产品目录屏幕包括自定义应用程序栏(MyAppBar
)和许多列表项的滚动视图(MyListItems
)。
以下是可视化的应用程序作为小部件树。
![一个顶层为MyApp的小部件树,下方为MyCatalog和MyCart。MyCart是叶子节点,但MyCatalog有两个子节点:MyAppBar和一个MyListItems列表。](/assets/images/docs/development/data-and-backend/state-mgmt/simple-widget-tree.png)
所以我们至少有5个Widget
的子类。它们中的许多都需要访问“属于”其他地方的状态。例如,每个MyListItem
都需要能够将自身添加到购物车。它可能还想查看当前显示的商品是否已在购物车中。
这引出了我们的第一个问题:我们应该将购物车的当前状态放在哪里?
上移状态
#在 Flutter 中,将状态保存在使用它的 Widget 之上是有意义的。
为什么?在像 Flutter 这样的声明式框架中,如果要更改 UI,则必须重建它。没有简单的方法可以执行MyCart.updateWith(somethingNew)
。换句话说,很难通过调用其上的方法来强制更改外部的小部件。即使您可以使此方法有效,您也会与框架对抗,而不是让它帮助您。
// 错误:不要这样做
void myTapHandler() {
var cartWidget = somehowGetMyCartWidget();
cartWidget.updateWith(item);
}
即使您使上述代码有效,您还必须在MyCart
小部件中处理以下内容:
// 错误:不要这样做
Widget build(BuildContext context) {
return SomeWidget(
// 购物车的初始状态。
);
}
void updateWith(Item item) {
// 以某种方式,您需要从此处更改UI。
}
您需要考虑 UI 的当前状态并将新数据应用于它。很难避免这种方式的错误。
在 Flutter 中,每次其内容发生更改时,您都会构造一个新的小部件。您使用MyCart(contents)
(构造函数)而不是MyCart.updateWith(somethingNew)
(方法调用)。因为您只能在其父级的构建方法中构造新的小部件,所以如果要更改contents
,它需要位于MyCart
的父级或更高位置。
// 正确
void myTapHandler(BuildContext context) {
var cartModel = somehowGetMyCartModel(context);
cartModel.add(item);
}
现在MyCart
只有一个构建任何 UI 版本的代码路径。
// 正确
Widget build(BuildContext context) {
var cartModel = somehowGetMyCartModel(context);
return SomeWidget(
// 只需使用购物车的当前状态构造一次UI。
// ···
);
}
在我们的示例中,contents
需要位于MyApp
中。每当它发生更改时,它都会从上方重建MyCart
(稍后会详细介绍)。因此,MyCart
不需要担心生命周期——它只声明在任何给定的contents
中显示的内容。当它发生更改时,旧的MyCart
小部件会消失,并被新的完全替换。
![与上面相同的小部件树,但现在我们在MyApp旁边显示了一个小的'购物车'徽章,这里有两条箭头。一条来自MyListItems之一到'购物车',另一条从'购物车'到MyCart小部件。](/assets/images/docs/development/data-and-backend/state-mgmt/simple-widget-tree-with-cart.png)
这就是我们所说的 Widget 不可变的意思。它们不会改变——它们会被替换。
现在我们知道将购物车的状态放在哪里了,让我们看看如何访问它。
访问状态
#当用户点击产品目录中的其中一件商品时,它会被添加到购物车中。但由于购物车位于MyListItem
之上,我们该如何做到这一点呢?
一个简单的选项是提供一个回调,MyListItem
在被点击时可以调用该回调。Dart 的函数是一等公民对象,因此您可以按任何方式传递它们。因此,在MyCatalog
中,您可以定义以下内容:
@override
Widget build(BuildContext context) {
return SomeWidget(
// 构造小部件,将对上面方法的引用传递给它。
MyListItem(myTapCallback),
);
}
void myTapCallback(Item item) {
print('用户点击了 $item');
}
这可以正常工作,但是对于需要从许多不同位置修改的应用程序状态,您必须传递许多回调——这很快就会变得很麻烦。
幸运的是,Flutter 具有允许小部件向其后代(换句话说,不仅仅是其子级,而是它们下面的任何小部件)提供数据和服务的机制。正如您对 Flutter 的预期一样,万物皆为 Widget™,这些机制只是特殊类型的小部件——InheritedWidget
、InheritedNotifier
、InheritedModel
等等。我们不会在这里介绍这些内容,因为对于我们正在尝试做的事情来说,它们有点低级。
相反,我们将使用一个与低级小部件一起工作但易于使用的包。它被称为provider
。
在使用provider
之前,不要忘记在您的pubspec.yaml
中添加对它的依赖。
要添加provider
包作为依赖项,请运行flutter pub add
:
flutter pub add provider
现在您可以import 'package:provider/provider.dart';
并开始构建。
使用provider
,您无需担心回调或InheritedWidgets
。但是您需要理解三个概念:
- ChangeNotifier
- ChangeNotifierProvider
- Consumer
ChangeNotifier
#ChangeNotifier
是Flutter SDK中包含的一个简单类,它向其侦听器提供更改通知。换句话说,如果某些内容是ChangeNotifier
,您可以订阅其更改。(对于熟悉此术语的人来说,它是一种 Observable。)
在provider
中,ChangeNotifier
是一种封装应用程序状态的方法。对于非常简单的应用程序,您可以使用单个ChangeNotifier
。在复杂的应用程序中,您将拥有多个模型,因此将拥有多个ChangeNotifiers
。(您根本不需要在provider
中使用ChangeNotifier
,但它是一个易于使用的类。)
在我们的购物应用程序示例中,我们希望在一个ChangeNotifier
中管理购物车的状态。我们创建一个扩展它的新类,如下所示:
class CartModel extends ChangeNotifier {
/// 购物车的内部私有状态。
final List<Item> _items = [];
/// 购物车中商品的不可修改视图。
UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
/// 所有商品的当前总价(假设所有商品的价格均为 42 美元)。
int get totalPrice => _items.length * 42;
/// 将[item]添加到购物车。这是从外部修改购物车的唯一方法。
void add(Item item) {
_items.add(item);
// 此调用告诉正在侦听此模型的小部件重建。
notifyListeners();
}
/// 从购物车中移除所有商品。
void removeAll() {
_items.clear();
// 此调用告诉正在侦听此模型的小部件重建。
notifyListeners();
}
}
唯一特定于ChangeNotifier
的代码是对notifyListeners()
的调用。每当模型以可能更改应用程序 UI 的方式更改时,都调用此方法。CartModel
中的其他所有内容都是模型本身及其业务逻辑。
ChangeNotifier
是flutter:foundation
的一部分,不依赖于 Flutter 中任何更高级别的类。它易于测试(您甚至不需要为此使用小部件测试)。例如,以下是CartModel
的简单单元测试:
test('添加商品会增加总成本', () {
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
放在哪里:在需要访问它的 Widget 之上。对于CartModel
来说,这意味着位于MyCart
和MyCatalog
之上的某个位置。
您不希望将ChangeNotifierProvider
放置得比必要的高(因为您不希望污染范围)。但在我们的例子中,唯一位于MyCart
和MyCatalog
之上的 Widget 是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('总价:${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: [
// 在这里使用SomeExpensiveWidget,无需每次都重建。
if (child != null) child,
Text('总价:${cart.totalPrice}'),
],
),
// 在这里构建昂贵的小部件。
child: const SomeExpensiveWidget(),
);
最佳实践是将您的Consumer
小部件尽可能地放在树的深处。您不希望仅仅因为某个地方的一些细节发生了更改而重建 UI 的大部分内容。
// 不要这样做
return Consumer<CartModel>(
builder: (context, cart, child) {
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Text('总价:${cart.totalPrice}'),
),
);
},
);
改为:
// 做这个
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Consumer<CartModel>(
builder: (context, cart, child) {
return Text('总价:${cart.totalPrice}');
},
),
),
);
Provider.of
#有时,您并不真正需要模型中的 数据 来更改 UI,但您仍然需要访问它。例如,ClearCart
按钮允许用户从购物车中移除所有内容。它不需要显示购物车的全部内容,它只需要调用clear()
方法。
我们可以为此使用Consumer<CartModel>
,但这将是浪费的。我们将要求框架重建不需要重建的小部件。
对于这种情况,我们可以使用Provider.of
,并将listen
参数设置为false
。
Provider.of<CartModel>(context, listen: false).removeAll();
在构建方法中使用以上代码不会在调用notifyListeners
时导致此小部件重建。
总结
#您可以查看本文中介绍的示例。如果您想要更简单的示例,请查看使用provider
构建的简单计数器应用程序的外观使用provider
构建。
通过阅读这些文章,您已经大大提高了创建基于状态的应用程序的能力。尝试自己使用provider
构建一个应用程序来掌握这些技能。
除非另有说明,否则本网站上的文档反映的是 Flutter 的最新稳定版本。页面最后更新于 2025-01-30。 查看源代码 或 报告问题。