简单的应用状态管理
一种简单的状态管理形式。
现在你已经了解了声明式 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,你可以订阅其更改。(对于熟悉该术语的人来说,它是一种可观察对象。)
在 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。它还会自动在不再需要该实例时调用 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,你也在每个 build 方法中获得它。
构建器的第二个参数是 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<CartModel> 来实现这一点,但这将是浪费的。我们会要求框架重建一个不需要重建的组件。
对于这种用例,我们可以使用 Provider.of,并将 listen 参数设置为 false。
Provider.of<CartModel>(context, listen: false).removeAll();
在 build 方法中使用上述行不会导致此组件在调用 notifyListeners 时重建。
整合所有概念
#你可以 查看本文中介绍的示例。如果你想要更简单的示例,请查看使用 provider 构建的简单计数器应用 的样子。
通过跟随这些文章,你大大提高了创建基于状态的应用的能力。尝试自己使用 provider 构建一个应用来掌握这些技能。