Flutter-跨组件状态共享

827 阅读5分钟

通过事件同步状态

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("添加商品"));
              }),
            ],
          );
        },),
      ),
    );
  }
}

原理图: image.png

Model变化后会自动通知ChangeNotifierProvider(订阅者),ChangeNotifierProvider内部会重新构建InheritedWidget,而依赖InteritedWidget的子Widget就会更新。这样使用的好处:

  1. 业务代码更关注数据,只需要更新Model,则UI会自动更新,而不用在改变状态后再去手动的调用setState来显式更新页面。
  2. 数据改变的消息传递被屏蔽了,无需手动的去处理状态改变事件的发布和订阅,一切都被封装在Provider中,减少工作量。
  3. 在大型复杂应用中,尤其是需要全局共享的状态多的,使用Provider将会大大简化我们的代码逻辑,降低出错的概率,提高开发效率。

优化

ChangeNotifierProvider有两个缺点:代码组织和性能问题

  1. 代码组织问题
Builder(builder: (context){
  var cart=ChangeNotifierProvider.of<CartModel>(context);
  return Text("总价: ${cart.totalPrice}");
})

优化点:

  1. 需要显式调用ChangeNotifierProvider.of,当app内部依赖的CartModel很多时,代码会很冗余。
  2. 语义不明确,由于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}");
)
  1. 性能问题 构建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实现