Flutter局部刷新原理

  • 概述

    在Flutter中,我们知道,刷新界面要调用setState方法,在一个界面中,通常只需要刷新某个组件或者某一部分组件,这种情况下调用父级State的setState方法会造成不必要的资源浪费。 在这种需求下,我们需要找到一个方式可以进行局部刷新。

  • 做法和原理

    其实局部刷新很简单,我们只需要把需要刷新的组件聚到一个StatefulWidget中,通过一个State来管理,然后刷新的时候调用这个State的setState方法即可完成针对这部分组件的刷新,父级和兄弟级的StatefulWidget都不会被引起rebuild。

    原理也很好理解,就是调用setState的流程,调用setState方法:

    @protected
    void setState(VoidCallback fn) {
      ...
      _element!.markNeedsBuild();
    }
    

    Element的markNeedsBuild方法会调用BuildOwner的scheduleBuildFor方法:

    void markNeedsBuild() {
      ...
      if (dirty)
        return;
      _dirty = true;
      owner!.scheduleBuildFor(this);
    }
    

    scheduleBuildFor方法会把当前element放入BuildOwner的_dirtyElements中:

    void scheduleBuildFor(Element element) {
      ...
      _dirtyElements.add(element);
      element._inDirtyList = true;
      ...
    }
    

    当下一个Frame到来时框架会调用WidgetsBinding的drawFrame方法:

    @override
    void drawFrame() {
      ...
      try {
        if (renderViewElement != null)
          buildOwner!.buildScope(renderViewElement!);
        //这里面是布局、合成层信息、绘制等流程
        super.drawFrame();
        buildOwner!.finalizeTree();
      } finally {
          ...
      }
      ...
    }
    

    这里会调用BuildOwner的buildScope方法:

    @pragma('vm:notify-debugger-on-exception')
    void buildScope(Element context, [ VoidCallback? callback ]) {
      ...
      try {
        ...
        _dirtyElements.sort(Element._sort);
          ...
        int dirtyCount = _dirtyElements.length;
        int index = 0;
        while (index < dirtyCount) {
          ...
          try {
            //重新构建
            _dirtyElements[index].rebuild();
          } catch (e, stack) {
            ...
          }
          index += 1;
          ...
        }
          ...
      } finally {
        for (final Element element in _dirtyElements) {
          assert(element._inDirtyList);
          element._inDirtyList = false;
        }
        //清空_dirtyElements
        _dirtyElements.clear();
        ...
      }
      ...
    }
    

    在buildScope方法中会循环 _dirtyElements,依次调用里面的element的rebuild方法进行构建,rebuild方法中又会调用performRebuild方法:

    @pragma('vm:prefer-inline')
    void rebuild() {
      ...
      performRebuild();
      ...
    }
    

    performRebuild方法是在StatefulElement和StatelessElement的共同父类ComponentElement中实现的,在这个方法中会调用build方法创建Widget:

    //StatefulElement中实现的build方法,可见会通过state的build方法生成
    @override
    Widget build() => state.build(this);
    //StatelessElement中实现的build方法
    @override
    Widget build() => widget.build(this);
    

    所以局部刷新原理的核心就是把需要刷新的区域收到一个State中,然后调用这个State的setState方法就会使当前的这个State的element变为dirty,把它放入需要重新构建的element集合中,在帧回调后会循环这个集合调用它的rebuild方法进行重新构建,因为我们更上一级的State并没有执行它的setState方法所以不会添加在需要重新构建的element集合中。

  • 关于get框架的应用

    get框架的局部刷新也是通过上面的原理完成的,下面我们来看看他是怎么封装的。

    首先它使用一个叫做GetxController的东西来提供统一刷新的api接口:

    abstract class GetxController extends DisposableInterface
        with ListenableMixin, ListNotifierMixin {
      void update([List? ids, bool condition = true]) {
        if (!condition) {
          return;
        }
        //全部刷新
        if (ids == null) {
          refresh();
        } else {
          //局部刷新
          for (final id in ids) {
            refreshGroup(id);
          }
        }
      }
    }
     

    在页面打开的时候会创建这个controller,然后通过调用这个controller的update方法执行局部构建,可以看到,局部构建需要一个id,这个id是什么时候绑定的呢?

    使用get框架的局部刷新需要把要刷新的组件们用一个GetBuilder包装起来,那这个GetBuilder构造时就可以传入一个id值,GetBuilder是一个StatefulWidget,他的State中的initState方法里调用了一个_subscribeToController方法:

    void _subscribeToController() {
      _remove?.call();
      _remove = (widget.id == null)
          //全部刷新的回调添加
          ? controller?.addListener(
              _filter != null ? _filterUpdate : getUpdate,
            )
          //局部刷新的回调添加
          : controller?.addListenerId(
              widget.id,
              _filter != null ? _filterUpdate : getUpdate,
            );
    }
    

    addListenerId方法中:

    Disposer addListenerId(Object? key, GetStateUpdate listener) {
      _updatersGroupIds![key] ??= [];
      _updatersGroupIds![key]!.add(listener);
      return () => _updatersGroupIds![key]!.remove(listener);
    }
    

    可以看到,这里根据id添加了一个回调函数,这里用的数组存放,可见可以通过指定同一个id的方式来实现几个区域联动刷新。

    回到上面的refreshGroup方法,内部会调用_notifyIdUpdate方法:

    void _notifyIdUpdate(Object id) {
      if (_updatersGroupIds!.containsKey(id)) {
        final listGroup = _updatersGroupIds![id]!;
        for (var item in listGroup) {
          item();
        }
      }
    }
    

    可见,在这里根据id查找并执行了所有相关的函数回调。

    那么函数回调是什么呢?_subscribeToController方法中,addListenerId方法添加的函数回调如果默认的话是getUpdate,它指向一个函数,这个函数在GetBuilderState依赖的mixin—GetStateUpdaterMixin中定义:

    void getUpdate() {
      if (mounted) setState(() {});
    }
    

    可以看到,正是在这里调用了setState来触发重新构建的,因为是在GetBuilderState中调用的setState方法,所以在GetBuilder之上的其他State是不会触发回调的,这和上面我们分析的原理是一样的。

  • 总结

    局部构建的原理就是用子State来拦截构建的范围,不把所有的组件树都放在一个大的State里面构建,通过调用子State的setState方法来实现针对子Widget树的重新构建,这样就实现了局部刷新。

    据此,我们当然可以不用get框架的局部刷新,完全可以自定义,我试着写了一下,有几个需要注意的点:

    1. setState一定要在需要局部刷新的State中调用;

    2. 调用setState的逻辑要通过一个函数暴露出来;

    3. 因为我们要保证随时可以刷新,所以我们需要一个随时获取且不会改变的对象来保存这个回调,相当于get的controller;

    4. 因为我们可能会有很多个局部需要刷新,它们必须独立且可以区分,所以我们需要保存回调函数的集合是一个可已按照key-value的形式来存放的集合,get中使用了String-List的形式,这样可以刷新好几块区域,我用的是一个String-dynamic的Map来存放,这个过程中发现了一个需要注意的点:

      Map的putIfAbsent方法的第二个参数规定是:

      V putIfAbsent(K key, V ifAbsent());
      

      如果使用这个方法设置回调函数,则需要在一个函数中返回这个回调函数才行。

  • 你可能感兴趣的:(Flutter局部刷新原理)