简单的应用状态管理
现在大家已经了解了 声明式的编程思维 和 短时 (ephemeral) 与应用 (app) 状态 之间的区别,现在可以学习如何管理简单的全局应用状态。
在这里,我们打算使用 provider
package。如果你是 Flutter 的初学者,而且也没有很重要的理由必须选择别的方式来实现(Redux、Rx、hooks 等等),那么这就是你应该入门使用的。provider
非常好理解而且不需要写很多代码。它也会用到一些在其它实现方式中用到的通用概念。
即便如此,如果你已经从其它响应式框架上积累了丰富的状态管理经验的话,那么可以在 状态 (State) 管理参考 中找到相关的 package 和教程。
示例
#为了演示效果,我们实现下面这个简单应用。
这个应用有两个独立的页面:一个类别页面和一个购物车页面(分别用 MyCatalog
,MyCart
widget 来展示)。虽然看上去是一个购物应用程序,但是你也可以和社交网络应用类比(把类别页面替换成朋友圈,把购物车替换成关注的人)。
类别页面包含一个自定义的 app bar (MyAppBar
)
以及一个包含元素列表的可滑动的视图 (MyListItems
)。
这是应用程序对应的可视化的 widget 树。
所以我们有至少 5 个 Widget
的子类。他们中有很多需要访问一些全局的状态。比如,MyListItem
会被添加到购物车中。但是它可能需要检查和自己相同的元素是否已经被添加到购物车中。
这里我们出现了第一个问题:我们把当前购物车的状态放在哪合适呢?
提高状态的层级
#在 Flutter 中,有必要将存储状态的对象置于 widget 树中对应 widget 的上层。
为什么呢?在类似 Flutter 的声明式框架中,如果你想要修改 UI,那么你需要重构它。并没有类似 MyCart.updateWith(somethingNew)
的简单调用方法。换言之,你很难通过外部调用方法修改一个 widget。即便你自己实现了这样的模式,那也是和整个框架不相兼容。
// BAD: DO NOT DO THIS
void myTapHandler() {
var cartWidget = somehowGetMyCartWidget();
cartWidget.updateWith(item);
}
即使你实现了上面的代码,也得处理 MyCart
widget 中的代码:
// 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 的状态,然后把最新的数据添加进去。但是这样的方式很难避免出现 bug。
在 Flutter 中,每次当 widget 内容发生改变的时候,你就需要构造一个新的。你会调用 MyCart(contents)
(构造函数),而不是 MyCart.updateWith(somethingNew)
(调用方法)。因为你只能通过父类的 build 方法来构建新 widget,如果你想修改 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
widget 就会消失,完全被新的 widget 替代。
这就是我们所说的 widget 是不可变的。因为它们会直接被替换。
现在我们知道在哪里放置购物车的状态,接下来看一下如何读取该状态。
读取状态
#当用户点击类别页面中的一个元素,它会被添加到购物车里。然而当购物车在 widget 树中,处于 MyListItem 的层级之上时,又该如何访问状态呢?
一个简单的实现方法是提供一个回调函数,当 MyListItem
被点击的时候可以调用。
Dart 的函数都是 first class 对象,所以你可以以任意方式传递它们。所以在 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 在 widget 中存在一种机制,能够为其子孙节点提供数据和服务。(换言之,不仅仅是它的子节点,所有在它下层的 widget 都可以)。就像你所了解的,
Flutter 中的 Everything is a Widget™。这里的机制也是一种 widget —InheritedWidget
, InheritedNotifier
,
InheritedModel
等等。我们这里不会详细解释他们,因为这些 widget 都太底层。
我们会用一个 package 来和这些底层的 widget 打交道,就是 provider
package。
在使用 provider
之前,请不要忘记在
pubspec.yaml
文件里加入依赖。
运行 flutter pub add
将 provider
添加为依赖:
flutter pub add provider
现在可以在代码里加入 import 'package:provider/provider.dart';
进而开始构建你的应用了/
provider
package 中,你无须关心回调或者 InheritedWidgets
。但是你需要理解三个概念:
- ChangeNotifier
- ChangeNotifierProvider
- Consumer
ChangeNotifier
#ChangeNotifier
是 Flutter SDK 中的一个简单的类。它用于向监听器发送通知。换言之,如果被定义为 ChangeNotifier
,你可以订阅它的状态变化。(这和大家所熟悉的观察者模式相类似)。
在 provider
中,ChangeNotifier
是一种能够封装应用程序状态的方法。对于特别简单的程序,你可以通过一个 ChangeNotifier
来满足全部需求。在相对复杂的应用中,由于会有多个模型,所以可能会有多个 ChangeNotifier
。
(不是必须得把 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 中任何高级别类。测试起来非常简单(你都不需要使用 widget 测试)。比如,这里有一个针对 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
widget 可以向其子孙节点暴露一个 ChangeNotifier
实例。它属于 provider
package。
我们已经知道了该把 ChangeNotifierProvider
放在什么位置:在需要访问它的 widget 之上。在 CartModel
里,也就意味着将它置于 MyCart
和 MyCatalog
之上。
你肯定不愿意把 ChangeNotifierProvider
放的级别太高(因为你不希望破坏整个结构)。但是在我们这里的例子中,MyCart
和 MyCatalog
之上只有 MyApp
。
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: const MyApp(),
),
);
}
请注意我们定义了一个 builder 来创建一个 CartModel
的实例。
ChangeNotifierProvider
非常聪明,它 不会 重复实例化 CartModel
,除非在个别场景下。如果该实例已经不会再被调用,
ChangeNotifierProvider
也会自动调用 CartModel
的 dispose()
方法。
如果你想提供更多状态,可以使用 MultiProvider
:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => CartModel()),
Provider(create: (context) => SomeOtherClass()),
],
child: const MyApp(),
),
);
}
Consumer
#现在 CartModel
已经通过 ChangeNotifierProvider
在应用中与 widget 相关联。我们可以开始调用它了。
完成这一步需要通过 Consumer
widget。
return Consumer<CartModel>(
builder: (context, cart, child) {
return Text('Total price: ${cart.totalPrice}');
},
);
我们必须指定要访问的模型类型。在这个示例中,我们要访问 CartModel
那么就写上 Consumer<CartModel>
。
Consumer
widget 唯一必须的参数就是 builder。当 ChangeNotifier
发生变化的时候会调用 builder 这个函数。(换言之,当你在模型中调用 notifyListeners()
时,所有相关的 Consumer
widget 的 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.
if (child != null) child,
Text('Total price: ${cart.totalPrice}'),
],
),
// Build the expensive widget here.
child: const SomeExpensiveWidget(),
);
最好能把 Consumer
放在 widget 树尽量低的位置上。你总不希望 UI 上任何一点小变化就全盘重新构建 widget 吧。
// 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>
来实现这个效果,不过这么实现有点浪费。因为我们让整体框架重构了一个无需重构的 widget。
所以这里我们可以使用 Provider.of
,并且将 listen
设置为 false
。
Provider.of<CartModel>(context, listen: false).removeAll();
在 build 方法中使用上面的代码,当 notifyListeners
被调用的时候,并不会使 widget 被重构。
把代码集成在一起
#你可以在文章中 查看这个示例。如果你想参考稍微简单一点的示例,可以看看 Counter 应用程序是如何
基于 provider
实现的。
通过跟着这些文章的学习,你已经大大提高了创建一个包含状态管理应用的能力。试着自己用 provider
构建一个应用来掌握这些技能吧!
除非另有说明,本文档之所提及适用于 Flutter 的最新稳定版本,本页面最后更新时间: 2024-05-20。 查看文档源码 或者 为本页面内容提出建议。