写点什么

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

作者:岛上码农
  • 2022 年 6 月 09 日
  • 本文字数:5612 字

    阅读完需:约 18 分钟

从源码解析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 中打上断点,来看一下运行效果。



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

StoreConnector 源码分析

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


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


发布于: 刚刚阅读数: 5
用户头像

岛上码农

关注

用代码连接孤岛,公众号@岛上码农 2022.03.03 加入

从南漂到北,从北漂到南的业余码农

评论

发布
暂无评论
从源码解析flutter_redux 的精准局部刷新_flutter_岛上码农_InfoQ写作社区