This is the 27th day of my participation in the August More Text Challenge

preface

For non-top-level stores, one interesting thing we found when testing is that StoreConnector built widgets do not rebuild the entire child component when the state changes, but only update components that depend on the converted object. This shows that StoreConnector can accurately locate which subcomponents depend on state variables, thus achieving accurate refresh and improving efficiency. This is similar to the select method of a Provider. In this article, we will analyze StoreConnector’s source code to see how to achieve accurate refresh.

validation

Let’s take a look at an example to verify our above statement, but without further ado, let’s look at the test code. We defined two buttons, one for like and one for favorites, and each click of the Action was scheduled to increase the corresponding number by one. The implementation of the two buttons is basically similar, except that the state-dependent data is different.

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('the local 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(
            'collection$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(
            'the thumb up$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,); }}Copy the code

Under normal circumstances, the entire sub-component is rebuilt after the status update. However, in actual operation, we found that only TextButton and its sub-component Text which depend on the state variable are rebuilt. We print the corresponding information in the build method of the two buttons, and then break the build of the TextButton (build method in its parent ButtonStyleButton) and Text components to see what happens.

According to the running result, when the button is clicked, the build methods of TextButton and Text are called, but the build methods of FavorButton and PraiseButton are not called (no corresponding information is printed). This indicates that StoreConnector has been updated locally with precision. Now let’s look at the source code and see what’s going on.

StoreConnector source code analysis

StoreConnector’s source code is very simple. StoreConnector itself inherits from StatelessWidget. In addition to the constructor and properties defined (all final), StoreConnector is a build method. It returns a _StoreStreamListener

component. Let’s see how this component works.
,>

@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,
  );
}
Copy the code

_StoreStreamListener is a StatefulWidget whose core implementation is in _StoreStreamListenerState

with the code shown below.
,>

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
  voidinitState() { widget.onInit? .call(widget.store); _computeLatestValue();if(widget.onInitialBuild ! =null) { WidgetsBinding.instance? .addPostFrameCallback((_) { widget.onInitialBuild! (_requireLatestValue); }); } _createStream();super.initState();
  }

  @override
  voiddispose() { 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! ;returnwidget.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) {
      returnvm ! = _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); }}Copy the code

The key Settings are in the initState method. In the initState method, if the onInit method is set, the store is passed to the initializer of the call state. For example, here is how we use the onInit property in our shopping list application.

onInit: (store) => store.dispatch(ReadOfflineAction()),
Copy the code

Next, you call the _computeLatestValue method, which actually gets the converted ViewModel object value stored in the ViewModel _latestValue property using the Converter method. Then, if the onInitialBuild method is defined, the initialization construct is done using the value of the ViewModel.

Finally, the _createStream method is called, which is critical!! In effect, the Store onChange event is transformed into a Stream

object with some filtering, In effect, only that part of the Store is listening for changes in the ViewModel object converted by the Converter method — local listening is implemented. The method for handling data changes is _handleChange. This essentially adds the changed ViewModel to the stream:

sink.add(vm);
Copy the code

Because the Build method uses the StremaBuilder component and listens for the _STREAM object, the Build method is triggered to rebuild when the ViewModel object after the state data transformation changes. This method will eventually call the method corresponding to the Builder property in StoreConnector. This part of the code happens to be a subordinate component of PraiseButton or FavorButton. That is why PraiseButton and FavorButton cannot be rebuilt when the state changes, because they are not subordinate components of StoreConnector. It’s the parent component.

That is, with StoreConnector, when the state changes, its subordinate components are refreshed later. Therefore, for performance reasons, we can do minimal wrapping. For example, we can wrap only the Text component so that the Container and TextButton will not be refreshed.

In order to compare, we only modified the code of PraiseButton. The actual breaking point found that the Container would not be refreshed when the “Like” button was clicked, while the TextButton would be refreshed. The analysis found that the appearance style of TextButton changed when the button was clicked. It’s not a Store state change. That is, by using StoreConnector to wrap child components with minimal scope, we can minimize the scope of the refresh to maximize performance. The code can be downloaded here (in the Partial_Refresh section) : Redux state management code.


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(
            'the thumb up$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))))); }}Copy the code

conclusion

Most of the time when we use third-party plug-ins, we just run the demo and use them directly. Yes, this can also achieve the purpose of functional implementation, but if you do encounter performance issues, often at a loss. Therefore, for some third-party plug-ins, it is worth being curious about how they are implemented and why.