前言
如下图,当state
发生变化,根据你所定义的ui渲染函数重新渲染ui。但是每个组件有自己的状态,这些状态有的是独立的有的是互相统一的,如何管理呢?
在学习本文之前,首先要区分开临时状态和应用状态,通常有以下划分:
临时状态
其他组件不会获取此状态,不会以复杂形式变化
这种情况就是临时状态,你可以通过StatefulWidget
来实现状态管理。
如:动画的进度,滚动条的位置,选项卡的选中项等。
应用状态
在APP的的多个组件之间,或者多个页面之间需要共享的状态称为应用状态。
这种情况就是应用状态,就涉及到了Flutter的应用状态管理。
如:用户登录信息,用户偏好,购物车信息等。
两者之间其实没有明确的分界线,如果你的APP中某个滚动条的位置会影响其他组件的显示状态,那这个就要定义为应用状态。
总之一句话,如果这块要给数据多个组件使用,就是应用状态;只给一个组件使用,就是临时状态。
实战
以饿了么举例:
-
在商品列表里有我加购的信息。
-
在购物车页面有我加购的信息
-
在结算页面,有我加购的信息
那么,这个加购信息就应该是应用状态。
进一步分析
我们应该把购物车状态交给谁来管理呢?很显然,根据状态提升,应由他们统一的父组件来管理。
如上图,当用户点击MyListItem
中的某一项时,会改变由MyApp
管理的状态cart
,进而引起MyCart
随之改变。这样MyCart
这块的代码只需要根据接受MyApp
的的cart
信息来更新UI,不需要考虑更新之类的代码,避免了很多不必要的麻烦。
另外,点击MyListItem
怎么样把cart
状态上传给MyApp
呢?你可能会想这样干:在父组件维护一个cart
数组,定义一些列操作cart
的回调函数,然后把这些回调函数一层层传给子组件,子组件想要操作cart
的时候,调用这些回调函数就可以啦。
@override
Widget build(BuildContext context) {
return SomeWidget(
//给MyListItem传一个回调函数
MyListItem(myTapCallback),
);
}
void myTapCallback(Item item) {
//回调函数:添加到购物车
添加的代码
}
是的,确实可以这样干。但是想一下,如果有很多状态需要在不同的地方进行操作,你就要定义很多很多回调函数,一层一层往下传。太麻烦了。
当然,Flutter也提供了InheritedWidget, InheritedNotifier, InheritedModel
等组件可以将数据自顶往下传,但是这些只能限定于该组件之内的后代共享数据,不能与其他组件共享数据。
这里我们将使用provider
库,简单易用。
首先在 pubspec.yaml
添加依赖:
dependencies:
flutter:
sdk: flutter
provider: ^6.0.0
使用provider
,你就不用考虑回调的问题,也不用考虑InheritedWidget
的问题。但是你要明白一下三个概念:
- ChangeNotifier
向他的订阅者发送消息通知。(类似
Observable
)
class CartModel extends ChangeNotifier {
final List<Item> _items = [];
/// An unmodifiable view of the items in the cart.
UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
/// 总价格
int get totalPrice => _items.length * 42;
///添加
void add(Item item) {
_items.add(item);
// 数据变化后,通知订阅者去刷新页面
notifyListeners();
}
/// 清除所有
void removeAll() {
_items.clear();
notifyListeners();
}
}
- ChangeNotifierProvider
可以向他后代提供
ChangeNotifier
供其使用
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: const MyApp(),
),
);
}
如果要同时提供给多个给孩子,可以用MultiProvider
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => CartModel()),
Provider(create: (context) => SomeOtherClass()),
],
child: const MyApp(),
),
);
}
- Consumer
需要使用状态的地方
每次ChangeNotifier
(本例是CartModel
)变动都会通知Consumer
执行builder
return Consumer<CartModel>(
builder: (context, cart, child) {
return Text("Total price: ${cart.totalPrice}");
},
);
上面的代码中,第2个参数就是CartModel
类型的对象。
第三个参数是一个优化项,如果你的一些组件比较大,某次变动以后,你不想整个刷新。可以参考如下代码:SomeExpensiveWidget
类型的对象并不会每次都重新渲染。
return Consumer<CartModel>(
builder: (context, cart, child) => Stack(
children: [
if (child != null) child,
Text("Total price: ${cart.totalPrice}"),
],
),
child: const SomeExpensiveWidget(),
);
注意
Consumer
使用范围应该尽量小,不要这样做:
//这样的话会引起HumongousWidget全部重新渲染,这可能是不必要的
return Consumer<CartModel>(
builder: (context, cart, child) {
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Text('Total price: ${cart.totalPrice}'),
),
);
},
);
应该这样:
//这样只会影响重新渲染最里面的这个Consumer
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Consumer<CartModel>(
builder: (context, cart, child) {
return Text('Total price: ${cart.totalPrice}');
},
),
),
);
代码
至此,你就可以正常使用cart了:
1. 购物车页面获取已加购的商品列表
可以这样:
```java
class _CartList extends StatelessWidget {
//如果cart有变动将会调用_CartList的build()
...........
Widget build(BuildContext context) {
var cart = context.watch<CartModel>();
...........
}
}
或者:
//使用Consumer只会刷新Consumer包裹部分的build()
class _CartTotal extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
....................
Consumer<CartModel>(
builder: (context, cart, child) =>
Text('\$${cart.totalPrice}', style: hugeStyle)),
.....................
]
),
),
);
}
}
- 添加
下面是商品列表中添加按钮的回调函数:使用了context.read()
。
有时候只需要操作状态,并不关注状态的变化,即不用监听状态,可以用read()
。
onPressed: isInCart
? null//如果在购物车,什么都不执行
: () {
//不在,先获取然后执行,用的是context.read
var cart = context.read<CartModel>();
//context.read 等同于Provider.of<T>(this, listen: false);
cart.add(item);
},
- 判断是否在购物车中
context.select()
允许你订阅你感兴趣的部分,只有感兴趣的部分变动时才会build
。
var isInCart = context.select<CartModel, bool>(
//select 返回的部分
(cart) => cart.items.contains(item),
);