Flutter数据共享之InheritedWidget、Provider

一:InheritedWidget是什么

InheritedWidget是flutter中非常重要的功能性组件,它提供了一种在widget树从上到下共享数据的方式。比如在应用的根widget中通过InheritedWidget共享了一个数据,那么我们便可以在任意子Widget中来获取该共享的数据。

如上图所示,比起普通的widget,逐级传递数据来看,InheritedWidget可以实现子控件跨级传递数据。这个特性在一些需要在整个widget树中共享数据的场景中非常方便,例如Flutter SDK正是通过InheritedWidget来共享应用主题Theme和语言环境Locale。代码上看区别:

当这个跨级跨的越来越大,传递数据越来越多时,InheritedWidget的特性就显得非常重要。

二:InheritedWidget怎么用

通过上面这张图来讲,在ShareDataWidget中,定义了一个data参数,由外部传入,在ShareDataWidget子控件中,ChildWidget2可以通过ShareDataWidget.of(context)?.data获取到数据。

先来看下InheritedWidget中比较重要的几个方法:

  1. context.dependOnInheritedWidgetOfExactType<ShareDataWidget>()

当在ChildWidget2中,调用该方法时,ChildWidget2和ShareDataWidget就会创建依赖关系,当ShareDataWidget的数据更改了,并且updateShouldNotify方法返回true时,ChildWidget2就会触发didChangeDependencies、build方法。

  1. context.getElementForInheritedWidgetOfExactType<ShareDataWidget>()!.widget as ShareDataWidget 这个方法和第一个方法的区别是,在子控件中调用该方法时,并不会将子控件和ShareDataWidget进行依赖关系绑定,所以当ShareDataWidget的数据更改,updateShouldNotify返回true时,也不会触发该控件的build和didChangeDependencies。(当然要注意写法,后面会说明)

上述代码中,dependOnInheritedElement方法中主要是注册了依赖关系,之后当InheritedWidget发生变化时,就会更新依赖他的子组件(调用didChangeDependencies、build方法),如果没有依赖的子组件也不会更新。

  1. bool updateShouldNotify(covariant ShareDataWidget oldWidget)

当ShareDataWidget的data变化了之后,InheritedWidget可以决定是否更新其子控件,当然也可以选择不更新,更新返回true,不更新返回false。

讲的有点抽象,看个例子:

当运行后,打印日志如下:

bash 复制代码
I/flutter (11723): rebuild common widget:0
I/flutter (11723): rebuild dependOnInheritedWidgetOfExactType widget:1
I/flutter (11723): rebuild getElementForInheritedWidgetOfExactType widget:1
I/flutter (11723): rebuild common widget:1
I/flutter (11723): rebuild dependOnInheritedWidgetOfExactType widget:2
I/flutter (11723): rebuild getElementForInheritedWidgetOfExactType widget:2
I/flutter (11723): rebuild common widget:2

-------------过了3秒后,调用setstate,打印日志如下-----------
I/flutter (11723): rebuild common widget:0
I/flutter (11723): rebuild dependOnInheritedWidgetOfExactType widget:2
I/flutter (11723): rebuild getElementForInheritedWidgetOfExactType widget:2
I/flutter (11723): rebuild common widget:2
I/flutter (11723): rebuild dependOnInheritedWidgetOfExactType widget:1

第一段日志,按照控件顺序,调用了build方法。当过了3秒后,调用了TestState.setState方法后,TestState的build方法被触发。

  1. getCommonWidget(0):无缓存,触发build,打印日志
  2. depend1:虽然有缓存,但是由于依赖了ShareDataWidget,且data发生变化,因此触发build。但是build顺序在最后。
  3. notDepend1:有缓存,但是不依赖ShareDataWidget,因此不触发build。
  4. common:有缓存,不触发build
  5. getDependWidget(1):因为依赖ShareDataWidget,data发生变化,触发build。
  6. getNotDependWidget(2)、getCommonWidget(2):不依赖ShareDataWidget, 且没有缓存,触发build。

对上述日志进行总结就是:

  • 父控件(TestState)的setState会造成build触发,触发TestState内的全局刷新
  • 如果子控件无缓存,每次父控件build都会触发子控件build。(getCommonWidget(0)、getDependWidget(1)、getNotDependWidget(2)、getCommonWidget(2))
  • 如果子控件有缓存,但是依赖了InheritedWidget,且数据发生变化,则触发build(depend1)。若不依赖InheritedWidget,则不触发build(common、notDepend1)。

所以如果是不依赖InheritedWidget的子widget,需要有缓存,否则还是会触发build。

三:InheritedWidget进一步优化

上述代码的例子中,如果TestState.data变化,我们只想更新依赖了ShareDataWidget的子控件,而现在更新data字段,需要调用TestState.setState方法,会导致没有缓存的子节点都被重新build,这很没有必要。解决办法就是缓存,但是我们平时写代码,不可能像上面示例代码中那样,在State中声明这样的控件去缓存,一个简单的办法就是,通过封装一个StatefulWidget,将InheritedWidget(ShareDataWidget)封装起来。

优化前 优化后
优化前,页面widget给InheritedWidget传入data,更新的时候调用页面Widget的setState,会造成未缓存的子widget都刷新。 用一个新封装widget来封装InheritedWidget,且是唯一的子组件。页面Widget给新封装widget传入T data和Widget child(图中蓝色的子widget),当页面Widget发现data变化时,通知新封装widget调用setState,重新构建InheritedWidget。

优化点在于:

  • setState范围缩小:data变化,不会调用页面Widget.setState,降低build成本,淡橙色的子widget都不会受影响。只会调用新封装widget.setState,只会重新构建InheritedWidget。
  • InheritedWidget子组件缓存:由于InheritedWidget都是页面Widget传入缓存在新封装widget里的,因此当InheritedWidget重建时,也只会重新构建依赖的子组件,不依赖的子组件则不rebuild。

那么问题来了,data变化的时候,页面Widget如何通知新封装Widget呢?当然实现的方式有很多种,比如ChangeNotifier,让T data继承ChangeNotifier,然后在数据更改的时候进行通知。当把data传入到新封装Widget中后,在initState里,给data添加listener,去监听数据变化。

scala 复制代码
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) {
    return context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>().data;
  }

  @override
  _ChangeNotifierProviderState<T> createState() => _ChangeNotifierProviderState<T>();
}

class _ChangeNotifierProviderState<T extends ChangeNotifier>
    extends State<ChangeNotifierProvider<T>> {
  @override
  void initState() {
    // 给model添加监听器
    widget.data.addListener(update);
    super.initState();
  }

  @override
  void didUpdateWidget(ChangeNotifierProvider<T> oldWidget) {
    //当Provider更新时,如果新旧数据不"==",则解绑旧数据监听,同时添加新数据监听
    if (widget.data != oldWidget.data) {
      oldWidget.data.removeListener(update);
      widget.data.addListener(update);
    }
    super.didUpdateWidget(oldWidget);
  }

  void update() {
    //如果数据发生变化(model类调用了notifyListeners),重新构建InheritedProvider
    setState(() => {});
  }

  @override
  Widget build(BuildContext context) {
    return InheritedProvider<T>(
      data: widget.data,
      child: widget.child,
    );
  }

  @override
  void dispose() {
    // 移除model的监听器
    widget.data.removeListener(update);
    super.dispose();
  }
}


// 一个通用的InheritedWidget,保存需要跨组件共享的状态
class InheritedProvider<T> extends InheritedWidget {
  InheritedProvider({required this.data, required Widget child});

  final T data;

  @override
  bool updateShouldNotify(InheritedProvider<T> old) {
    //在此简单返回true,则每次更新都会调用依赖其的子孙节点的`didChangeDependencies`。
    return true;
  }
}

再画个更清楚的流程图,当然画的不全,比如removeListener这些都没画进去,就是大概表示一下意思:

讲了这么多,看一下最终代码使用的例子:

less 复制代码
class _ProviderRouteState extends State<ProviderRoute> {
  @override
  Widget build(BuildContext context) {
    return Center(
        child: ChangeNotifierProvider<CartModel>(
            data: CartModel(),
            child: Builder(builder: (context) {
              return Column(children: <Widget>[
                Builder(builder: (context) {
                  var cart = ChangeNotifierProvider.of<CartModel>(context);
                  return Text("总价: ${cart.totalPrice}");
                }),
                Builder(builder: (context) {
                  print("ElevatedButton build"); //在后面优化部分会用到
                  return ElevatedButton(
                      child: Text("添加商品"),
                      onPressed: () {
                        //给购物车中添加商品,添加后总价会更新
                        ChangeNotifierProvider.of<CartModel>(context)
                            .add(Item(20.0, 1));
                      });
                })
              ]);
            })));
  }
}

上面这个代码中,添加商品的按钮,每次点击后,会导致CardModel刷新,但是由于按钮自身依赖了InheritedWidget,所以也会导致rebuild,这里可以优化一下,让其不依赖。

至此上面讲的,基本上是Provider(一个用于管理状态的包)的底层原理。

上面的例子看似简单,不能体现Provider的强大,但是如果当我们的业务变得很复杂,一个页面内部层级比较深,状态比较多,各个子组件不断嵌套,那么如果要逐级传递数据的话,就会显得不那么优雅,用Provider就能很好的解决跨级传递数据问题。如果在App内,是多个页面共享数据的话,那么则需要将Provider设置的层级更高一些,比如在main.dart中。

Flutter社区还有其他用于状态管理的包,例如:Scoped ModelReduxMobXBLoC。等我一一研究再分享。

上述描述有疏漏的,请大家指正。

相关推荐
pengyu2 小时前
系统化掌握Flutter开发之导航器(Navigator)(一):页面跳转的“指挥官”
android·flutter·dart
帅次3 小时前
Flutter FloatingActionButton 从核心用法到高级定制
android·flutter·macos·ios·kotlin·android-studio
嘟嘟叽3 小时前
flutter 图片资源路径管理
开发语言·javascript·flutter
pengyu6 小时前
系统化掌握Flutter开发之路由(Route)(一):筑基之旅
android·flutter·dart
张风捷特烈9 小时前
Flutter&Flame 游戏实践#22 | 全平台游戏盒#1
android·flutter·游戏开发
恋猫de小郭10 小时前
Android PC 要来了?Android 16 Beta3 出现 Enable desktop experience features 选项
android·前端·flutter
北岛贰20 小时前
爆肝两个月,我用flutter开发了一款免费音乐app
flutter·dart
A0微声z1 天前
从0到1掌握Flutter(三)Dart语法
flutter
亿码归一码1 天前
【flutter】flutter 环境搭建
flutter