从源码解析flutter_redux 的精准局部刷新

前言

对于非顶级的 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,然后直接上手就用。确实,这样也能够达到功能实现的目的,但是如果真的遇到性能上面的问题的时候,往往不知所措。因此,对于有些第三方插件,还是有必要保持好奇心,了解其中的实现机制,做到知其然知其所以然

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,607评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,239评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,960评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,750评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,764评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,604评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,347评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,253评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,702评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,893评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,015评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,734评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,352评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,934评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,052评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,216评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,969评论 2 355

推荐阅读更多精彩内容