美文网首页Flutter
Flutter局部刷新原理

Flutter局部刷新原理

作者: 就叫汉堡吧 | 来源:发表于2022-03-11 08:32 被阅读0次
    • 概述

      在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<Object>? 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] ??= <GetStateUpdate>[];
        _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局部刷新原理

        本文链接:https://www.haomeiwen.com/subject/qrtxdrtx.html