Flutter ChangeNotifierProvider凭什么能实现局部刷新?

讲在前面

对于刚上手开发Flutter的同学,想要实现一个Widget的刷新,除了使用StatefulWidget+setState方法,似乎没有什么更好的方式;

更深入一点之后发现,可以使用一些状态管理库来实现Widget的刷新,似乎更加方便而且规范了;

比如官方提供的Provider状态管理库,我们可以使用其提供的ChangeNotifierProvider来实现刷新,在我们的状态类(继承ChangeNotifier)中调用notifyListeners之后,我们在ChangeNotifierProviderchild内有声明context.watch()或使用Consumer/Selector等包裹的地方就会进行刷新;

比较神奇的地方在于,在一个复杂的Widget树中,这些库可以帮助我实现某些Widget的局部刷新,避免一些高频次的整体重建(尽管framework源码中对于Element树的构建有足够多的逻辑优化,我们还是需要尽量避免无意义的Widget刷新)

为了搞清楚这个机制的实现原理,这里我们自制一个简易版Provider作为切入口来分析:

先来个demo图示:

然后看看代码内容

示例代码

视图 & 状态
dart 复制代码
class SamplePage extends StatelessWidget {
  const SamplePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("SamplePage"),
      ),
      body: MyChangeNotifierProvider<SampleModel>(_buildBody(), SampleModel()),
    );
  }

  _buildBody() {
    return SizedBox.expand(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Builder(builder: (context) {
            return Text(
                "CountA is ${(MyInheritedProvider.of(context, listen: true).model as SampleModel).count.toString()}");
          }),
          Builder(builder: (context) {
            return Text(
                "CountB is ${(MyInheritedProvider.of(context, listen: false).model as SampleModel).count.toString()}");
          }),
          Builder(builder: (context) {
            return GestureDetector(
              onTap: () {
                (MyInheritedProvider.of(context, listen: false).model
                        as SampleModel)
                    .countIncrease();
              },
              child: const Text("launch"),
            );
          })
        ],
      ),
    );
  }
}
dart 复制代码
class SampleModel extends MyChangeNotifier {
  int count = 0;

  countIncrease() {
    count++;
    notifyListener();
  }
}
自制Provider相关
dart 复制代码
class MyChangeNotifier {
  Function? notifyFunc;

  registerListener(Function func){
    notifyFunc = func;
  }

  notifyListener() {
    notifyFunc?.call();
  }
}
dart 复制代码
class MyInheritedProvider<T extends MyChangeNotifier> extends InheritedWidget {
  final T model;

  const MyInheritedProvider(
    this.model, {
    Key? key,
    required Widget child,
  }) : super(key: key, child: child);

  static MyInheritedProvider of(BuildContext context, {bool listen = false}) {
    if (listen) {
      final MyInheritedProvider? result =
          context.dependOnInheritedWidgetOfExactType<MyInheritedProvider>();
      assert(result != null, 'No MyInheritedProvider found in context');
      return result!;
    } else {
      final MyInheritedProvider? result = context
          .getElementForInheritedWidgetOfExactType<MyInheritedProvider>()!
          .widget as MyInheritedProvider?;
      assert(result != null, 'No MyInheritedProvider found in context');
      return result!;
    }
  }

  @override
  bool updateShouldNotify(MyInheritedProvider old) {
    return true;
  }
}
dart 复制代码
class MyChangeNotifierProvider<T extends MyChangeNotifier>
    extends StatefulWidget {
  final Widget child;
  final T model;

  const MyChangeNotifierProvider(this.child, this.model, {Key? key})
      : super(key: key);

  @override
  State<MyChangeNotifierProvider> createState() =>
      _MyChangeNotifierProviderState();
}

class _MyChangeNotifierProviderState extends State<MyChangeNotifierProvider> {
  doSetState() {
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return MyInheritedProvider(
      widget.model,
      child: widget.child,
    );
  }

  @override
  void didUpdateWidget(
      covariant MyChangeNotifierProvider<MyChangeNotifier> oldWidget) {
    super.didUpdateWidget(oldWidget);
    widget.model.registerListener(doSetState);
  }
}

上面这一大坨代码,就是我们Demo的全部代码,分为两部分

  • 我们自己的业务代码(第一部分)
  • 我们自制的ChangeNotifierProvider(第二部分)

这里我们为了更好的与官方的Provider库对应上,所以自制的Provider框架类前面都加了个my,方便理解;

这里我们说明下具体类的功能

类名 功能
sample_page 页面类,其中展示了3个Text,前两个引用了SampleModel中的count值,但是A进行了监听,B没有
sample_model 状态类,继承了MyChangeNotifier,其中只有一个count变量和改变count的简单方法
my_change_notifier 仅仅存储一个Function,合适的时机调用该Function
my_inherited_provider 核心类,继承于InheritedWidget,其中存放一个数据模型,泛型限制继承于MyChangeNotifier。提供了一个获取当前类的方法,入参区分是否进行监听(关联依赖)
my_change_notifier_provider 核心类,继承于StatefulWidget,接收一个Widget和一个数据模型,最终在State的build方法中均传入MyInheritedProvider

简单介绍完之后,我们来具体分析一下:

SamplePage

首先是页面(SamplePage),比较简单,写法跟ChangeNotifierProvider一样,在页面之上包裹一个我们的MyChangeNotifierProvider,里面是一个列表,里面有三个Text,前两个是展示count数值,后一个是点击后去增长count值的;

注意这里我们的几个Widget都是用Builder包裹了一层,这里提前说明一下是为了使用其BuildContext去获取组件树上的MyInheritedProvider。


MyChangeNotifierProvider

然后我们来看一看MyChangeNotifierProvider类:

我们直接看其State类,很简单:

在didUpdateWidget钩子函数中将setState方法注册进入我们的MyChangeNotifier中;

didUpdateWidget 方法在以下情况下会被调用:

  • 当与该 State 对象关联的 Widget 重新构建并创建一个新的 Widget 实例时。
  • 当父 Widget 改变并重新构建该 StatefulWidget 时,Flutter 框架会调用 didUpdateWidget 方法。

build方法将外部传入的model和child都传入MyInheritedProvider中;

总的来说这个类的主要工作就是注册一下刷新方法,供状态类在某个时机下调用;并且在build时对于传入的Widget包裹了一层MyInheritedProvider返回。看起来这就是一个简单的中介类。


MyInheritedProvider

进入MyInheritedProvider,这是最核心的部分,此类继承于InheritedWidge;

InheritedWidget最重要的功能之一在于,可以通过BuildContext的dependOnInheritedWidgetOfExactType或getElementForInheritedWidgetOfExactType方法获取祖先组件树中的InheritedWidget类型的Widget,并且前者的方法中有一个依赖注册的功能,这点我们会分析到;

另外一点也很重要,InheritedWidget对应的Element是InheritedElement,其父类是ProxyElement,它是继承与ComponentElement的,不过它的build方法并不像StatelessElement一样是调用自己Widget的build方法,而是直接返回了Widget的child变量


好了,分析完这两个核心类,我们来看看代码运行后的表现以及为何如此。

回到我们的SamplePage,我们点击了"launch"按钮,此时调用了SampleModel中的countIncrease方法:将count++,并且调用notifyListener方法;

这个方法对应的就是_MyChangeNotifierProviderState中的setState方法,这时候build方法执行,重新返回了一个MyInheritedWidget;

可能刚接触Flutter的同学就出现疑惑了,理论上来说StatefulWidget调用了setState方法之后,其子类会进行刷新,而我们传入的三个Text都是StatefulWidget的子类(StatefulWidget - MyInheritedWidget - 3个Text),为什么会只刷新了其中的一个Text呢?


源码分析

我们开始追踪源码;

我们要知道一个前提:刷新Widget会先进入Element的rebuild方法。然后是performRebuild方法,这个方法Element没做什么,交由具体子类去实现。StatefulWidget的Element的是ComponentElement,所以我们来看看它的具体实现:

这里的build方法,即我们的MyInheritedProvider:

注意这里的build方法,是ComponentElement独有方法,这里返回的MyInheritedProvider即updateChild方法传入的built参数,这里解释下这3个入参:

变量 类型 含义
_child Element 当前Element持有的子Elmenet,第一次执行时或上一次没有child时为null
built Widget 即调用自身build返回的Widget对象,build方法具体实现交由子类(比如我们常写的StatelessWidget中的build方法)
slot Object slot 是一个用于标识元素在其父元素中的位置或角色的抽象概念。它通常用于复杂的布局逻辑,其中子元素之间的关系并不仅仅是一个简单的线性列表,这个点本文不做具体解释,此部分不影响本文分析内容

接着看updateChild方法,我们先总结一下这个方法的工作:就是传入build返回的Widget和之前加载Element树时已生成的子Element做各种比较,判断要不要重新通过Widget生成一个新的Element,还是说仍然使用之前的Element子类,只是做一下更新Widget动作;

这里我们把不重要的代码先删除掉,图示分析一下这个方法中都做了什么:

回到我们的代码中,我们点击了"launch"按钮,执行了setState,然后进入performRebuild,又进入updateChild方法,这里child是第一次运行时就生成的InheritedElement,newWidget是传入的MyInheritedProvider;

  • 条件1,判断不进入,因为build方法返回的是一个新的MyInheritedProvider,跟之前Element持有的并不是同一个对象
  • 条件2,判断进入,因为运行时类型是一样的(并且我们没有给Widget传入key参数)

那么这里就执行了child.update(newWidget)

update在Element类中只做了一个重新赋值_widget的操作:

我们还是要看具体子类有没有重写该方法,InheritedElement->ProxyElement->ComponentElement->Element

三个子类中只有ProxyElement进行了重写:

这里1稍微放一放,我们看看2的逻辑;

这里我们要记住我们当前执行的已经是在InheritedElement对象中的方法了,因为它跟StatefulElement一样也是ComponentElement的子类,最终也会走到上面的performRebuild方法,然后调用自己的build()方法,返回一个built传入updateChild方法;

好了,这里就是我们要重点分析的地方了,为什么没有刷新我们的业务Widget (这里就暂且称我们SamplePage中传入MyChangeNotifierProvider的child为业务Widget,即下图_buildBody中Widget)

一、我们setState刷新的是State类,而State#build方法中返回的MyInheritedProvider中的child不是重新创建的,而是一开始外部传入StatefulWidget中存储的;

二、记得上面StatefulElement这一层执行到了child.update(newWidget),进入了InheritedElement这一层,它的update方法中仅替换了Element持有的Widget对象(Element没有重新创建),然后进入了ComponentElement#performRebuild方法,这里执行了自己的build方法去获取一个Widget,这个Widget是什么呢?回顾一下

它就是我们的业务Widget,一直作为child变量存储在ProxyWidget中,这里的ProxyElement#build方法只是将其拿了出来,并没有重新创建一个Widget;

三、那么看到我们的updateChild方法中(上翻一下updateChild方法图示),自然就进入了条件分支1中,因为等式两边都是我们的业务Widget(同一个对象)

所以,组件树从上向下更新的过程中到了这里就中断了,不会向下再进行了;


现在我们要来研究一下最后的问题:为什么组件Widget中的其中一个Text可以被刷新?

我们来看下两个Text分别怎么展示自己的text内容的:

这个参数区分是使用了什么方法来获取组件树中的MyInheritedProvider

CountA使用的方法:BuildContext# dependOnInheritedWidgetOfExactType

CountB使用的方法:BuildContext# getElementForInheritedWidgetOfExactType

这里直接说明一下区别,前者方法比后者多一个功能:

先在组件树祖先中找到指定类型的InheritedElement,然后将当前的Element依赖到对应的InheritedElement中,使用一个Map容器(_dependents)来存储;

那么这些容器里的Element又是在哪里被拿出来使用的呢?是怎么使用的呢?

还记得之前讲的setState之后,执行到了MyInheritedProvider对应Element的update方法吗(拿出来再看一眼)

我们进去看一看都有什么动作

进入了ProxyElement的updated方法,不过它的子类InheritedElement重写了这个方法,看一眼

还记得这个方法吗,我们的MyInheritedProvider继承于InheritedWidget,必须要重写这个方法,来决定是否应该通知依赖,我们为了简单直接return了true(可以根据具体业务决定是否通知),所以逻辑执行了super.updated。接着向里看

终于,看到了熟悉的方法markNeedsBuild,把当前Element标记为需要更新,后续则通过BuildOwner展开了组件的刷新逻辑(这部分等同于StatefulWidget的State中调用了setState,不做展开了)


说在最后

总结一下:

到此为止,我们终于搞定了一个丐版的自制可局部刷新的状态管理框架~🎉🎉,至于官方ChangeNotifierProvider的实现逻辑,其实实现逻辑不尽相同,我们后续再专门做一篇分析;

这个整体的实现核心逻辑就是Flutter框架中提供的InheritedWidget组件,这个组件的重要性不亚于我们最常使用的StatelessWidget、StatefulWidget,了解了其核心逻辑,我们也可以使用它来写出一些优雅的框架等;

最后贴一下上述的demo,里面添加了部分注释,大家可以clone下来debug一下增加理解。

以上如有错误,欢迎指出!

相关推荐
子兮曰3 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰3 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万3 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝3 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
三十而立洋3 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
卡布鲁3 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试