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。等我一一研究再分享。

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

相关推荐
微祎_25 分钟前
Flutter for OpenHarmony:单词迷宫一款基于 Flutter 构建的手势驱动字母拼词游戏,通过滑动手指连接字母路径来组成单词。
flutter·游戏
ujainu1 小时前
护眼又美观:Flutter + OpenHarmony 鸿蒙记事本一键切换夜间模式(四)
android·flutter·harmonyos
ujainu1 小时前
让笔记触手可及:为 Flutter + OpenHarmony 鸿蒙记事本添加实时搜索(二)
笔记·flutter·openharmony
一只大侠的侠1 小时前
Flutter开源鸿蒙跨平台训练营 Day 13从零开发注册页面
flutter·华为·harmonyos
一只大侠的侠1 小时前
Flutter开源鸿蒙跨平台训练营 Day19自定义 useFormik 实现高性能表单处理
flutter·开源·harmonyos
恋猫de小郭2 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
一只大侠的侠7 小时前
Flutter开源鸿蒙跨平台训练营 Day 10特惠推荐数据的获取与渲染
flutter·开源·harmonyos
renke336410 小时前
Flutter for OpenHarmony:色彩捕手——基于HSL色轮与感知色差的交互式色觉训练系统
flutter
子春一12 小时前
Flutter for OpenHarmony:构建一个 Flutter 四色猜谜游戏,深入解析密码逻辑、反馈算法与经典益智游戏重构
算法·flutter·游戏
铅笔侠_小龙虾13 小时前
Flutter 实战: 计算器
开发语言·javascript·flutter