Flutter 入门与实战(六十四):这篇很长,为了性能,你忍一下 —— 从源码看 flutter_redux精准刷新

1,233 阅读5分钟

这是我参与8月更文挑战的第27天,活动详情查看:8月更文挑战

前言

对于非顶级的 Store,我们测试的时候会发现一个有趣的现象,那就是 StoreConnector 构建的 Widget 在状态发生改变的时候,并不会重建整个子组件,而是只更新依赖于 converter 转换后对象的组件。这说明 StoreConnector 能够精准地定位到哪个子组件依赖状态变量,从而实现精准刷新,提高效率。这和 Providerselect 方法类似。 本篇我们就来分析一下 StoreConnector 的源码,看一下是如何实现精准刷新的。

验证

我们先看一个示例,来验证一下我们上面的说法,话不多说,先看测试代码。我们定义了两个按钮,一个点赞,一个收藏,每次点击调度对应的 Action 使得对应的数量加1。两个按钮的实现基本类似,只是依赖状态的数据不同。

class DynamicDetailWrapper extends StatelessWidget {
  final store = Store<PartialRefreshState>(
    partialRefreshReducer,
    initialState: PartialRefreshState(favorCount: 0, praiseCount: 0),
  );
  DynamicDetailWrapper({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print('build');
    return StoreProvider<PartialRefreshState>(
        store: store,
        child: Scaffold(
          appBar: AppBar(
            title: Text('局部 Store'),
          ),
          body: Stack(
            children: [
              Container(height: 300, color: Colors.red),
              Positioned(
                  bottom: 0,
                  height: 60,
                  width: MediaQuery.of(context).size.width,
                  child: Row(
                    mainAxisSize: MainAxisSize.min,
                    mainAxisAlignment: MainAxisAlignment.spaceAround,
                    children: [
                      _PraiseButton(),
                      _FavorButton(),
                    ],
                  ))
            ],
          ),
        ));
  }
}

class _FavorButton extends StatelessWidget {
  const _FavorButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print('FavorButton');
    return StoreConnector<PartialRefreshState, int>(
      builder: (context, count) => Container(
        alignment: Alignment.center,
        color: Colors.blue,
        child: TextButton(
          onPressed: () {
            StoreProvider.of<PartialRefreshState>(context)
                .dispatch(FavorAction());
          },
          child: Text(
            '收藏 $count',
            style: TextStyle(color: Colors.white),
          ),
          style: ButtonStyle(
              minimumSize: MaterialStateProperty.resolveWith((states) =>
                  Size((MediaQuery.of(context).size.width / 2), 60))),
        ),
      ),
      converter: (store) => store.state.favorCount,
      distinct: true,
    );
  }
}

class _PraiseButton extends StatelessWidget {
  const _PraiseButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print('PraiseButton');
    return StoreConnector<PartialRefreshState, int>(
      builder: (context, count) => Container(
        alignment: Alignment.center,
        color: Colors.green[400],
        child: TextButton(
          onPressed: () {
            StoreProvider.of<PartialRefreshState>(context)
                .dispatch(PraiseAction());
          },
          child: Text(
            '点赞 $count',
            style: TextStyle(color: Colors.white),
          ),
          style: ButtonStyle(
              minimumSize: MaterialStateProperty.resolveWith((states) =>
                  Size((MediaQuery.of(context).size.width / 2), 60))),
        ),
      ),
      converter: (store) => store.state.praiseCount,
      distinct: false,
    );
  }
}

按正常的情况,状态更新后应该是整个子组件rebuild,但是实际运行我们发现只有依赖于状态变量的TextButton和其子组件 Text进行了 rebuild。我们在两个按钮的 build 方法打印了对应的信息,然后在 TextButtonbuild 方法在其父类ButtonStyleButton中)和 Text 组件的 build 中打上断点,来看一下运行效果。

屏幕录制2021-08-26 下午8.47.54.gif

从运行结果看,点击按钮的时候 TextButtonTextbuild 方法均被调用了,但是 FavorButtonPraiseButtonbuild 方法并没有调用(未打印对应的信息)。这说明 StoreConnector 进行了精准的局部更新。接下来我们从源码看看是怎么回事?

StoreConnector 源码分析

StoreConnector 的源码很简单,本身 StoreConnector 继承自 StatelessWidget,除了定义的构造方法和属性(均为 final)外,就是一个 build 方法,只是 build方法比较特殊,返回的是一个_StoreStreamListener<S, ViewModel>组件。来看看这个组件是怎么回事。

@override
Widget build(BuildContext context) {
  return _StoreStreamListener<S, ViewModel>(
    store: StoreProvider.of<S>(context),
    builder: builder,
    converter: converter,
    distinct: distinct,
    onInit: onInit,
    onDispose: onDispose,
    rebuildOnChange: rebuildOnChange,
    ignoreChange: ignoreChange,
    onWillChange: onWillChange,
    onDidChange: onDidChange,
    onInitialBuild: onInitialBuild,
  );
}

_StoreStreamListener是一个StatefulWidget,核心实现在_StoreStreamListenerState<S, ViewModel>中,代码如下所示。

class _StoreStreamListenerState<S, ViewModel>
    extends State<_StoreStreamListener<S, ViewModel>> {
  late Stream<ViewModel> _stream;
  ViewModel? _latestValue;
  ConverterError? _latestError;

  // `_latestValue!` would throw _CastError if `ViewModel` is nullable,
  // therefore `_latestValue as ViewModel` is used.
  // https://dart.dev/null-safety/understanding-null-safety#nullability-and-generics
  ViewModel get _requireLatestValue => _latestValue as ViewModel;

  @override
  void initState() {
    widget.onInit?.call(widget.store);

    _computeLatestValue();

    if (widget.onInitialBuild != null) {
      WidgetsBinding.instance?.addPostFrameCallback((_) {
        widget.onInitialBuild!(_requireLatestValue);
      });
    }

    _createStream();

    super.initState();
  }

  @override
  void dispose() {
    widget.onDispose?.call(widget.store);

    super.dispose();
  }

  @override
  void didUpdateWidget(_StoreStreamListener<S, ViewModel> oldWidget) {
    _computeLatestValue();

    if (widget.store != oldWidget.store) {
      _createStream();
    }

    super.didUpdateWidget(oldWidget);
  }

  void _computeLatestValue() {
    try {
      _latestError = null;
      _latestValue = widget.converter(widget.store);
    } catch (e, s) {
      _latestValue = null;
      _latestError = ConverterError(e, s);
    }
  }

  @override
  Widget build(BuildContext context) {
    return widget.rebuildOnChange
        ? StreamBuilder<ViewModel>(
            stream: _stream,
            builder: (context, snapshot) {
              if (_latestError != null) throw _latestError!;

              return widget.builder(
                context,
                _requireLatestValue,
              );
            },
          )
        : _latestError != null
            ? throw _latestError!
            : widget.builder(context, _requireLatestValue);
  }

  ViewModel _mapConverter(S state) {
    return widget.converter(widget.store);
  }

  bool _whereDistinct(ViewModel vm) {
    if (widget.distinct) {
      return vm != _latestValue;
    }

    return true;
  }

  bool _ignoreChange(S state) {
    if (widget.ignoreChange != null) {
      return !widget.ignoreChange!(widget.store.state);
    }

    return true;
  }

  void _createStream() {
    _stream = widget.store.onChange
        .where(_ignoreChange)
        .map(_mapConverter)
        // Don't use `Stream.distinct` because it cannot capture the initial
        // ViewModel produced by the `converter`.
        .where(_whereDistinct)
        // After each ViewModel is emitted from the Stream, we update the
        // latestValue. Important: This must be done after all other optional
        // transformations, such as ignoreChange.
        .transform(StreamTransformer.fromHandlers(
          handleData: _handleChange,
          handleError: _handleError,
        ));
  }

  void _handleChange(ViewModel vm, EventSink<ViewModel> sink) {
    _latestError = null;
    widget.onWillChange?.call(_latestValue, vm);
    final previousValue = vm;
    _latestValue = vm;

    if (widget.onDidChange != null) {
      WidgetsBinding.instance?.addPostFrameCallback((_) {
        if (mounted) {
          widget.onDidChange!(previousValue, _requireLatestValue);
        }
      });
    }

    sink.add(vm);
  }

  void _handleError(
    Object error,
    StackTrace stackTrace,
    EventSink<ViewModel> sink,
  ) {
    _latestValue = null;
    _latestError = ConverterError(error, stackTrace);
    sink.addError(error, stackTrace);
  }
}

关键的设置都在 initState 方法中。在 initState 方法中,如果设置了 onInit 方法,就会将 store 传递过去调用状态的初始化方法,例如下面就是我们在购物清单应用中对 onInit 属性的使用。

onInit: (store) => store.dispatch(ReadOfflineAction()),

接下来是调用_computeLatestValue方法,实际是通过converter方法获得转换后的ViewModel对象的值,这个值存储在ViewModel _latestValue属性中。然后是,如果定义了 onInitialBuild 方法,就会使用 ViewModel 的值做初始化构造。

最后调用了_createStream 方法,这个方法很关键!!!实际上就是吧 StoreonChange 事件按照一定的过滤方式转变了成了 Stream<ViewModel>对象,其实相当于是只监听了 Store 中经过 converter 方法转换后那一部分ViewModel 对象的变化——也就是实现了局部监听。处理数据变化的方法为_handleChange。实际上就是将变化后的 ViewModel 加入到流中:

sink.add(vm);

因为 build 方法中使用的是 StremaBuilder 组件,并且会监听_stream 对象,因此当状态数据转换后的 ViewModel 对象发生改变时,会触发 build 方法进行重建。而这个方法最终会调用 StoreConnector 中的 builder 属性对应的方法。这部分代码正好是 PraiseButtonFavorButton 的下级组件,这就是为什么状态发生变化时 PraiseButtonFavorButton不会被重建的原因,因为他们不是StoreConnector 的下级组件,而是上级组件。

也就是说, 使用StoreConnector这种方式时,当状态发生改变后,之后刷新它的下级组件。因此,从性能考虑,我们可以做最小范围的包裹,比如这个例子,我们可以只包裹 Text 组件,这样 ContainerTextButton 也不会被刷新了。

为了对比,我们只修改了 PraiseButton 的代码,实际打断点发现点击点赞按钮的Container不会被刷新,而TextButton 会刷新,分析发现是TextButton 的外观样式在点击的时候改变导致的,并不是Store状态改变导致。也就是说,通过最小范围使用 StoreConnector 包裹子组件,我们可以将刷新的范围缩到最小,从而最大限度地提升性能。具体代码可以到这里下载(partial_refresh部分):Redux 状态管理代码


class _PraiseButton extends StatelessWidget {
  const _PraiseButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print('PraiseButton');
    return Container(
      alignment: Alignment.center,
      color: Colors.green[400],
      child: TextButton(
        onPressed: () {
          StoreProvider.of<PartialRefreshState>(context)
              .dispatch(PraiseAction());
        },
        child: StoreConnector<PartialRefreshState, int>(
          builder: (context, count) => Text(
            '点赞 $count',
            style: TextStyle(color: Colors.white),
          ),
          converter: (store) => store.state.praiseCount,
          distinct: false,
        ),
        style: ButtonStyle(
            minimumSize: MaterialStateProperty.resolveWith(
                (states) => Size((MediaQuery.of(context).size.width / 2), 60))),
      ),
    );
  }
}

总结

很多时候我们在使用第三方插件的时候,都是跑跑 demo,然后直接上手就用。确实,这样也能够达到功能实现的目的,但是如果真的遇到性能上面的问题的时候,往往不知所措。因此,对于有些第三方插件,还是有必要保持好奇心,了解其中的实现机制,做到知其然知其所以然