现在大家已经了解了
在这里,我们打算使用 provider
package。如果你是 Flutter 的初学者,而且也没有很重要的理由必须选择别的方式来实现(Redux、Rx、hooks 等等),那么这就是你应该入门使用的。provider
非常好理解而且不需要写很多代码。它也会用到一些在其它实现方式中用到的通用概念。
即便如此,如果你已经从其它响应式框架上积累了丰富的状态管理经验的话,那么可以在 状态 (State) 管理参考 中找到相关的 package 和教程。
链接:https://flutter.cn/docs/development/data-and-backend/state-mgmt/options
1. 示例
为了演示效果,我们实现下面这个简单应用。
程序有三个独立的页面:一个登陆提示,一个类别页面,一个购物车页面(分别用 MyLoginScreen
, MyCatalog
,MyCart
widget 来展示)。虽然看上去是一个购物应用程序,但是你也可以和社交网络应用类比(把类别页面替换成朋友圈,把购物车替换成关注的人)。
类别页面包含一个自定义的 app bar (MyAppBar
) 以及一个包含元素列表的可滑动的视图 (MyListItems
)。
这是应用程序对应的可视化的 widget 树。
所以我们有至少 6 个 Widget
的子类。他们中有很多需要访问一些全局的状态。比如,MyListItem
会被添加到购物车中。但是它可能需要检查和自己相同的元素是否已经被添加到购物车中。
这里我们出现了第一个问题:我们把当前购物车的状态放在哪合适呢?
2. 提高状态的层级
在 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。
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 替代。
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');
}
InheritedWidget
,
InheritedNotifier
,
InheritedModel
等等。我们这里不会详细解释他们,因为这些 widget 都太底层。
provider
package 。
provider
package 中,你无须关心回调或者
InheritedWidgets
。但是你需要理解三个概念:
-
ChangeNotifier
-
ChangeNotifierProvider
-
Consumer
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). 现在全部商品的总价格(假设他们加起来 $42)
int get totalPrice => _items.length * 42;
/// Adds [item] to cart. This is the only way to modify the cart from outside. 将 [item] 添加到购物车。这是唯一一种能从外部改变购物车的方法。
void add(Item item) {
_items.add(item);
// 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;
cart.addListener(() {
expect(cart.totalPrice, greaterThan(startingPrice));
});
cart.add(Item('Dash');
});
ChangeNotifierProvider
widget 可以向其子孙节点暴露一个
ChangeNotifier
实例。它属于
provider
package。
ChangeNotifierProvider
放在什么位置:在需要访问它的 widget 之上。在
CartModel
里,也就意味着将它置于
MyCart
和
MyCatalog
之上。
ChangeNotifierProvider
放的级别太高(因为你不希望破坏整个结构)。但是在我们这里的例子中,
MyCart
和
MyCatalog
之上只有
MyApp
。
void main() {
runApp(
ChangeNotifierProvider(
builder: (context) => CartModel(),
child: MyApp(),
),
);
}
CartModel
的实例。
ChangeNotifierProvider
非常聪明,它
不会 重复实例化
CartModel
,除非在个别场景下。如果该实例已经不会再被调用,
ChangeNotifierProvider
也会自动调用
CartModel
的
dispose()
方法。
MultiProvider
:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(builder: (context) => CartModel()),
Provider(builder: (context) => SomeOtherClass()),
],
child: MyApp(),
),
);
}
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
相关的 builder 方法都会被调用。)
context
。在每个 build 方法中都能找到这个参数。
ChangeNotifier
的实例。它是我们最开始就能得到的实例。你可以通过该实例定义 UI 的内容。
child
,用于优化目的。如果
Consumer
下面有一个庞大的子树,当模型发生改变的时候,该子树 并不会 改变,那么你就可以仅仅创建它一次,然后通过 builder 获得该实例。
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: 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}'),
),
);
},
);
// 这么写
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Consumer<CartModel>(
builder: (context, cart, child) {
return Text('Total price: ${cart.totalPrice}');
},
),
),
);
ClearCart
按钮能够清空购物车的所有商品。它不需要显示购物车里的内容,只需要调用
clear()
方法。
Consumer<CartModel>
来实现这个效果,不过这么实现有点浪费。因为我们让整体框架重构了一个无需重构的 widget。
Provider.of
,并且将
listen
设置为
false
。
Provider.of<CartModel>(context, listen: false).add(item);
notifyListeners
被调用的时候,并不会使 widget 被重构。
provider
的时候,别忘了先在
pubspec.yaml
中添加相应的依赖。
name: my_name
description: Blah blah blah.
# ...
dependencies:
flutter:
sdk: flutter
provider: ^3.0.0
dev_dependencies:
# ...
import 'package:provider/provider.dart';
,开始写代码吧。