通过事件同步状态
Flutter开发中,状态管理是一个永恒的话题。一般原则是:如果状态是组件私有,则由组件自己管理;如果状态要跨组件共享,则该状态应该由各个组件共同的父元素来管理。跨组件共享状态管理的方式较多,比如全局事件EventBus,他是一个观察者模式的实现,通过它可以实现跨组件状态的同步:状态持有方(发布者)负责更新、发布状态,状态使用方(观察者)监听状态改变事件来执行一些操作。
实例:
enum Event{
login,
... //省略其他事件
}
// 登录状态改变后发布状态改变事件
bus.emit(Event.login);
void onLoginChanged(e){
//登录状态变化处理逻辑
}
@override
void initState() {
//订阅登录状态改变事件
bus.on(Event.login,onLogin);
super.initState();
}
@override
void dispose() {
//取消订阅
bus.off(Event.login,onLogin);
super.dispose();
}
总结:
- 必须显式的定义各种事件,不好管理。
- 订阅者必须显式的注册状态改变回调,也必须在组件销毁时手动的去解绑回调以避免内存泄漏。
为了解决这个问题,FLutter社区中提供PRivider包基于InheritedWidget思想实现一套跨组件状态共享解决方案。
Provider
1、自实现ProVider 首先需要一个能够保存共享数据的InheritedWidget,由于具体业务数据不可预期,为了通用性,可以使用泛性,定义一个通用的InheritedProvider类,它继承自InheritedWidget:
// 一个通用的InheritedWidget,保存需要跨组件共享的状态
class InheritedProvider<T> extends InheritedWidget {
InheritedProvider({
required this.data,
required Widget child,
}) : super(child: child);
final T data;
@override
bool updateShouldNotify(InheritedProvider<T> old) {
//在此简单返回true,则每次更新都会调用依赖其的子孙节点的`didChangeDependencies`。
return true;
}
}
在数据发生变化的时候重新构建InheritedPrivider,那么问题来了:1、数据发生变化怎么通知;2谁来构建InheritedPrivider
为了贴近Flutter开发,使用ChangeNotifier类,它继承自Listenable,也实现了Flutter风格的发布者-订阅者模式。ChangeNotifier定义如下:
class ChangeNotifier implements Listenable {
List listeners=[];
@override
void addListener(VoidCallback listener) {
//添加监听器
listeners.add(listener);
}
@override
void removeListener(VoidCallback listener) {
//移除监听器
listeners.remove(listener);
}
void notifyListeners() {
//通知所有监听器,触发监听器回调
listeners.forEach((item)=>item());
}
... //省略无关代码
}
可以通过addListerer()和removeListener()来添加、移除监听器(订阅者);通过notifyListeners()可以触发所有监听器回调。
将需要共享的状态放到一个Model类中,然后让它继承自ChangeNotifier,这样当共享的状态改变时,只需要调用notifyListeners()来通知订阅者,然后由订阅者重新构建InheritedProvider,这样也处理了第二个问题:
class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget {
ChangeNotifierProvider({
Key? key,
this.data,
this.child,
});
final Widget child;
final T data;
//定义一个便捷方法,方便子树中的widget获取共享数据
static T of<T>(BuildContext context) {
final type = _typeOf<InheritedProvider<T>>();
final provider = context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>();
return provider!.data;
}
@override
ChangeNotifierProviderState<T> createState() => ChangeNotifierProviderState<T>();
}
该类继承StatefulWidget,然后定义一个of()静态方法供子嘞方便获取Widget树中InheritedProvider中保存的共享状态(model),下面实现该类对应的ChangeNotifierProviderState类
class ChangeNotifierProviderState<T extends ChangeNotifier> extends State<ChangeNotifierProvider<T>> {
void update(){
//如果数据发生变化(model类调用了notifyListeners),重新构建InheritedProvider
setState(() {
});
}
@override
void didUpdateWidget(ChangeNotifierProvider<T> oldWidget) {
//当Provider更新时,如果新旧数据不"==",则解绑旧数据监听,同时添加新数据监听
if (widget.data != oldWidget.data){
oldWidget.data.removeListener(update);
widget.data.addListener(update);
}
super.didUpdateWidget(oldWidget);
}
@override
void initState() {
//给model添加监听器
widget.data.addListener(update);
super.initState();
}
@override
void dispose() {
//移除监听器
widget.data.removeListener(update);
super.dispose();
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return InheritedProvider<T>(
data: widget.data,
child: widget.child
);
}
}
可以看到ChangeNotifierProvierState类主要作用就是监听到共享状态model改变时重新构建Widget树,注意,在ChangeNotifierProViderState类中调用setState()方法,widget.child始终是同一个,所以执行build时,InheritedProvider的child的引用始终也是同一个子widget,也就是widget.child并不会重新build,相当于进行了缓存,如果ChangeNotifierProvider父级的Widget重新build则其传入的child便有可能发生变化。
使用实例
//创建商品数据模型
class CartItem{
CartItem(this.price, this.count);
double price;
int count;
}
//创建购物车数据管理模型
class CartModel extends ChangeNotifier{
final List<CartItem> _items = [];
//禁止其他方式修改购物车商品信息
// UnmodifiableListView<CartItem> get items => UnmodifiableListView(_items);
UnmodifiableListView<CartItem> get items {
return UnmodifiableListView(_items);
}
//计算总价
double get totalPrice {
return _items.fold(0, (previousValue, element){
return previousValue + element.count*element.price;
});
}
//添加商品、触发计算和绘制
void add(CartItem item){
_items.add(item);
notifyListeners();
}
}
class ProviderRoute extends StatefulWidget{
@override
ProvideRouteState createState() {
return ProvideRouteState();
}
}
class ProvideRouteState extends State<ProviderRoute>{
@override
Widget build(BuildContext context) {
// TODO: implement build
return Center(
child: ChangeNotifierProvider<CartModel>(
data: CartModel(),//记录data
child: Builder(builder: (context){
return Column(
children: [
Builder(builder: (context){
var cart = ChangeNotifierProvider.of<CartModel>(context);
return Text("总价:${cart.totalPrice}");
}),
Builder(builder: (context){
return ElevatedButton(onPressed: (){
ChangeNotifierProvider.of<CartModel>(context).add(CartItem(20.0, 3));
}, child: Text("添加商品"));
}),
],
);
},),
),
);
}
}
原理图:
Model变化后会自动通知ChangeNotifierProvider(订阅者),ChangeNotifierProvider内部会重新构建InheritedWidget,而依赖InteritedWidget的子Widget就会更新。这样使用的好处:
- 业务代码更关注数据,只需要更新Model,则UI会自动更新,而不用在改变状态后再去手动的调用setState来显式更新页面。
- 数据改变的消息传递被屏蔽了,无需手动的去处理状态改变事件的发布和订阅,一切都被封装在Provider中,减少工作量。
- 在大型复杂应用中,尤其是需要全局共享的状态多的,使用Provider将会大大简化我们的代码逻辑,降低出错的概率,提高开发效率。
优化
ChangeNotifierProvider有两个缺点:代码组织和性能问题
- 代码组织问题
Builder(builder: (context){
var cart=ChangeNotifierProvider.of<CartModel>(context);
return Text("总价: ${cart.totalPrice}");
})
优化点:
- 需要显式调用ChangeNotifierProvider.of,当app内部依赖的CartModel很多时,代码会很冗余。
- 语义不明确,由于ChangeNotifierProvider是订阅者,那么依赖CartModel的Widget就是订阅者,也就是状态的消费者,如果用Builder构建,语义就不明确,如果能使用一个具有明确语义的Widget比如Consumer这样最终的代码语义将会很明确,只需要看到Consumer就知道它是依赖某个跨组件或全局的状态。
实例:
// 这是一个便捷类,会获得当前context和指定数据类型的Provider
class Consumer<T> extends StatelessWidget {
Consumer({
Key? key,
required this.builder,
}) : super(key: key);
final Widget Function(BuildContext context, T? value) builder;
@override
Widget build(BuildContext context) {
return builder(
context,
ChangeNotifierProvider.of<T>(context),
);
}
}
//使用
Consumer<CartModel>(
builder: (context, cart)=> Text("总价: ${cart.totalPrice}");
)
- 性能问题 构建ElevatedButton的Builder中调用了ChangeNotifierPrivider.of,也就是依赖了Widget树上面的InheritedWidget(InheritedProvider)Widget,所以添加商品完成后,CarModel发生变化,会通知ChangeNotifierProvider,ChangeNotifierProvider则会重新构建子树,所以InheritedProvider将会更新,此时依赖的子Widget将会被重新构建。
处理方式: 调用dependOnInheritedWidgetOfExactType()和getElementForInheritedWidgetOfExactType()的区别就是前者会注册依赖关系,后者不会,所以只需将ChangeNotifierProvider.of的实现改成:
//添加一个listen参数,表示是否建立依赖关系
static T of<T>(BuildContext context, {bool listen = true}) {
final type = _typeOf<InheritedProvider<T>>();
final provider = listen
? context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>()
: context.getElementForInheritedWidgetOfExactType<InheritedProvider<T>>()?.widget
as InheritedProvider<T>;
return provider.data;
}
//应用
Column(
children: <Widget>[
Consumer<CartModel>(
builder: (BuildContext context, cart) =>Text("总价: ${cart.totalPrice}"),
),
Builder(builder: (context) {
print("ElevatedButton build");
return ElevatedButton(
child: Text("添加商品"),
onPressed: () {
// listen 设为false,不建立依赖关系
ChangeNotifierProvider.of<CartModel>(context, listen: false)
.add(Item(20.0, 1));
},
);
})
],
)
此时ElevatedButton 不会再重新构建了,但是总价仍会更新,因为Consumer中调用ChangeNotifierProvider.of时,listen值默认为true,还是会有依赖关系。Provider只是一个简版,但是功能不全,实战中还是建议使用Provider Package。
以下是一些Flutter社区评分较高的Provider:
| 包名 | 介绍 |
|---|---|
| Provider & Scoped Model | 两个包都是基于InheritedWidget的,原理相似 |
| Redux | 是web开发中React生态链中Redux包的Flutter实现 |
| MobX | 是Web开发中React生态链中MobX包的Flutter实现 |
| BLoC | 是BLoC模式的Flutter实现 |