简单的应用状态管理
现在您已经了解了声明式 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(contents)
(构造函数)而不是MyCart.updateWith(somethingNew)
(方法调用)。因为您只能在其父级的构建方法中构建新部件,所以如果要更改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
。(您根本不需要在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
#现在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,但您仍然需要访问它。例如,“清空购物车”按钮希望允许用户从购物车中删除所有内容。它不需要显示购物车的內容,只需要调用clear()
方法。
我们可以为此使用Consumer<CartModel>
,但这将是浪费的。我们将要求框架重建一个不需要重建的部件。
对于此用例,我们可以使用Provider.of
,并将listen
参数设置为false
。
Provider.of<CartModel>(context, listen: false).removeAll();
在构建方法中使用以上代码不会导致此部件在调用notifyListeners
时重建。
将所有内容整合在一起
#您可以查看本文中介绍的示例。如果您想要更简单的示例,请查看使用provider
构建的简单计数器应用的样子。
通过阅读这些文章,您已经大大提高了创建基于状态的应用的能力。尝试自己使用provider
构建一个应用以掌握这些技能。
除非另有说明,否则本网站上的文档反映了 Flutter 的最新稳定版本。页面上次更新于 2024-05-03。 查看源代码 或 报告问题.