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

2,451 阅读10分钟

讲在前面

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

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

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

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

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

先来个demo图示:

image.png

然后看看代码内容

示例代码

视图 & 状态
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"),
            );
          })
        ],
      ),
    );
  }
}
class SampleModel extends MyChangeNotifier {
  int count = 0;

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

  registerListener(Function func){
    notifyFunc = func;
  }

  notifyListener() {
    notifyFunc?.call();
  }
}
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;
  }
}
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(第二部分)

image.png

这里我们为了更好的与官方的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类:

image.png

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

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

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

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

build方法将外部传入的modelchild都传入MyInheritedProvider中;

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


MyInheritedProvider

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

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

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

image.png

image.png


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

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

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

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


源码分析

我们开始追踪源码;

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

image.png

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

image.png

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

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

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

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

image.png

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

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

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

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

image.png

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

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

image.png

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

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

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

image.png

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

image.png

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

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

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

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


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

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

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

CountA使用的方法:BuildContext# dependOnInheritedWidgetOfExactType

CountB使用的方法:BuildContext# getElementForInheritedWidgetOfExactType

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

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

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

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

image.png

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

进入了ProxyElementupdated方法,不过它的子类InheritedElement重写了这个方法,看一眼 image.png

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

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


说在最后

总结一下:

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

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

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

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