Flutter Bloc source code analysis

Those who have used Bloc can definitely feel that the Bloc framework has made a very clear division of the development page, and the framework has forced two development modes.

  • Bloc mode: This mode is divided into four layers 
    • bloc: logic layer
    • state: data layer
    • event: all interaction events
    • view: page
  • Cubit mode: This mode is divided into three layers 
    • cubit: logic layer
    • state: data layer
    • view: page

The author is still very old-fashioned in the division of levels. The state layer is directly written inside the framework, and this layer must be separated out; I feel that if it hadn't been pitted by the Cthulhu code of large-scale projects, there should be no such thing. deep obsession

This state layer is added, I think it is quite necessary, because once a certain page maintains a lot of state, mixing state variables and logic methods together will cause a lot of headaches in later maintenance.

Be critical...

  • You may be in the group and often see some old brothers say: Bloc is a layer of Provider encapsulation.

    • Here I confirm: this is true, Bloc does seal the Provider
    • But only use the child node in the Provider to query the InheritedElement data of the nearest parent node and the side-by-side layout function of the top-level Widget. The most classic refresh mechanism of the Provider is completely useless!
  • I think the author of Bloc may be a little confused about the refresh mechanism of Provider

    • Even if the bloc framework uses a line in the build widget:  Provider.of<T>(context, listen: true)  or removes e.markNeedsNotifyDependents()  , I wouldn't say it. . .
    • The Bloc framework has done something that makes me very confused. The callback in the _startListening method calls  e.markNeedsNotifyDependents()  , which is completely useless ! Because Provider.of<T>(context, listen: true) is not used  to add child Elements to InheritedElement, it is a lonely refresh! In order to verify my idea, I debugged the notifyClients method of the framework layer. When calling emit or yield to refresh, the map of _dependents is always empty, hey. . .
  • I complained a lot above, not my opinion on bloc

    • I also used Bloc for a long time, used the process in depth, made some optimizations for its usage, and wrote a code generation plugin for it, and I paid some time and energy for it.
    • But: the code does not lie, all the good and the bad are in it, you can feel it with your heart.

Why do you say it's complicated?

When I looked at the source code of the Provider before, I had some headaches, and the internal logic was indeed a bit complicated, but the general process was straightened out, and after the refresh logic was clear, it was a hearty feeling! After the pain, there is a huge sense of satisfaction, and I am amazed that the Provider is proficient in using various APIs of the Framework layer, and then implements a wonderful refresh mechanism!

Then, as mentioned above, I did spend some energy on Bloc to optimize its use, then read his source code, and then think about the Provider source code I read before, and suddenly there is a huge sense of gap.

In my opinion, such a well-known open source library, the above point is completely avoidable; perhaps it is this inexplicable high expectation that makes me have this gap. . .

By the way, maybe the author of Bloc deliberately left a Provider refresh mechanism in Bloc as an easter egg!

Suddenly the lump is gone!

use

The usage is introduced here, and some adjustments have been made to the official usage.

For the process of adjusting your mentality, you can refer to: Flutter_bloc usage analysis --- Sao Nian, are you still using bloc!

The following will directly write the adjusted writing method.

plugin

Because the writing method generated by the official plug-in is a bit different from the adjusted writing method, and the official plug-in does not support the generation of view layers and related settings, here I created a plug-in to improve the related functions

Please note that the Wrap code and prompt code snippet refer to the official plugin rules

The Wrap Widget rule comes from: intellij_generator_plugin

The quick code generation rule comes from:  intellij_generator_plugin

  • Search for flutter bloc in Android Studio 

  • Generate template code

  • Support to modify the suffix

  • Wrap Widget (alt + enter):RepositoryProvider,BlocConsumer,BlocBuilder,BlocProvider,BlocListener

  • Enter  bloc  to generate shortcut code snippets

usage

The plugin can generate two pattern codes: Bloc and Cubit; take a look

Cubit mode

  • view
class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (BuildContext context) => CounterCubit(),
      child: Builder(builder: (context) => _buildPage(context)),
    );
  }

  Widget _buildPage(BuildContext context) {
    final cubit = BlocProvider.of<CounterCubit>(context);

    return Container();
  }
}
  • cubit
class CounterCubit extends Cubit<CounterState> {
  CounterCubit() : super(CounterState().init());
}
  • state
class CounterState {
  CounterState init() {
    return CounterState();
  }

  CounterState clone() {
    return CounterState();
  }
}

Bloc mode

  • view: an initialization event is added by default
class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (BuildContext context) => CounterBloc()..add(InitEvent()),
      child: Builder(builder: (context) => _buildPage(context)),
    );
  }

  Widget _buildPage(BuildContext context) {
    final bloc = BlocProvider.of<CounterBloc>(context);

    return Container();
  }
}
  • bloc
class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterState().init());

  @override
  Stream<CounterState> mapEventToState(CounterEvent event) async* {
    if (event is InitEvent) {
      yield await init();
    }
  }

  Future<CounterState> init() async {
    return state.clone();
  }
}
  • event
abstract class CounterEvent {}

class InitEvent extends CounterEvent {}
  • state
class CounterState {
  CounterState init() {
    return CounterState();
  }

  CounterState clone() {
    return CounterState();
  }
}

Summarize

The structure of Bloc and Cubit mode is very clearly divided. Because there are multiple layers of structure division, there must be corresponding template codes and files. Without the help of plug-ins, it will be very uncomfortable to write these template codes every time; here for everyone I wrote this plugin, if there is any bug, please give feedback in time. . .

I will not repeat how to use it here. For details, please refer to: Flutter_bloc usage analysis---Sao Nian, are you still using bloc!

Pre-knowledge

To understand the principle of Bloc, you need to understand the relevant knowledge of Stream first.

StreamController, StreamBuilder: The combination of these two can also easily refresh local widgets. Let's see how to use them.

  • view: The Stream stream must have a closed operation. Here, StatefulWidget needs to be used, and its dispose callback is required
class StreamPage extends StatefulWidget {
  const StreamPage({Key? key}) : super(key: key);

  @override
  _StreamPageState createState() => _StreamPageState();
}

class _StreamPageState extends State<StreamPage> {
  final logic = StreamLogic();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Bloc-Bloc范例')),
      body: Center(
        child: StreamBuilder<StreamState>(
          initialData: logic.state,
          stream: logic.stream,
          builder: (context, snapshot) {
            return Text(
              '点击了 ${snapshot.data!.count} 次',
              style: TextStyle(fontSize: 30.0),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => logic.increment(),
        child: Icon(Icons.add),
      ),
    );
  }

  @override
  void dispose() {
    logic.dispose();
    super.dispose();
  }
}
  • logic: The Stream data source is generic, and the basic type can be used directly. The entity is used here to expand more data later
class StreamLogic {
  final state = StreamState();

  // 实例化流控制器
  final _controller = StreamController<StreamState>.broadcast();

  Stream<StreamState> get stream => _controller.stream;

  void increment() {
    _controller.add(state..count = ++state.count);
  }

  void dispose() {
    // 关闭流控制器,释放资源
    _controller.close();
  }
}
  • state
class StreamState {
  int count = 0;
}
  • renderings

In fact, after reading the above use, you will find that there are several troublesome places

  • A series of objects that need to create a Stream
  • The Stream stream must have a close operation, so use StatefulWidget
  • StreamBuilder needs to write three parameters, which is very troublesome

The author of Bloc borrowed the InheritedProvider control of the Provider to solve the above pain points

refresh mechanism

The refresh mechanism of Bloc is very simple. The above Stream operation basically clarifies its core refresh mechanism, but the author of Bloc has done some encapsulation, let's take a look

The charm of BlocProvider

BlocProvider is a very important control. The simplification of refresh parameters and the closing of Stream are related to it, because it encapsulates InheritedProvider in a Provider; however, in my opinion, he is still a very attractive control

  • BlocProvider: The source code of BlocProvider is very simple, the following is the source code of this class
class BlocProvider<T extends BlocBase<Object?>>
    extends SingleChildStatelessWidget with BlocProviderSingleChildWidget {
  /// {@macro bloc_provider}
  BlocProvider({
    Key? key,
    required Create<T> create,
    this.child,
    this.lazy,
  })  : _create = create,
        _value = null,
        super(key: key, child: child);

  BlocProvider.value({
    Key? key,
    required T value,
    this.child,
  })  : _value = value,
        _create = null,
        lazy = null,
        super(key: key, child: child);

  /// Widget which will have access to the [Bloc] or [Cubit].
  final Widget? child;
    
  final bool? lazy;

  final Create<T>? _create;

  final T? _value;

  static T of<T extends BlocBase<Object?>>(
    BuildContext context, {
    bool listen = false,
  }) {
    try {
      return Provider.of<T>(context, listen: listen);
    } on ProviderNotFoundException catch (e) {
      if (e.valueType != T) rethrow;
      throw FlutterError(
        '''
        BlocProvider.of() called with a context that does not contain a $T.
        No ancestor could be found starting from the context that was passed to BlocProvider.of<$T>().

        This can happen if the context you used comes from a widget above the BlocProvider.

        The context used was: $context
        ''',
      );
    }
  }

  @override
  Widget buildWithChild(BuildContext context, Widget? child) {
    final value = _value;
    return value != null
        ? InheritedProvider<T>.value(
            value: value,
            startListening: _startListening,
            lazy: lazy,
            child: child,
          )
        : InheritedProvider<T>(
            create: _create,
            dispose: (_, bloc) => bloc.close(),
            startListening: _startListening,
            child: child,
            lazy: lazy,
          );
  }

  static VoidCallback _startListening(
    InheritedContext<BlocBase> e,
    BlocBase value,
  ) {
    final subscription = value.stream.listen(
      (dynamic _) => e.markNeedsNotifyDependents(),
    );
    return subscription.cancel;
  }
}
  • The difference between BlocProvider and BlocProvider.value

    • Looking at the source code above, we can see that BlocProvider.value does not automatically close the Stream 
      • So BlocProvider.value should not be used in normal single page, it can be used for global Bloc instance
    • For single page Bloc, please use BlocProvider to create Bloc or Cubit
  • create is an externally instantiated XxxBloc, which is finally passed into the InheritedProvider

    • create is an instance of XxxBloc passed in from the outside
    • The instance is directly passed into the InheritedProvider, which is involved in the Provider, and is finally stored in _InheritedProviderScopeElement, _startListening is also the content of the Provider 
    • Seriously, the logic in _startListening is useless 
      • The api markNeedsNotifyDependents is specially made by the Provider author for the refresh of the Provider sub-Element. It must be matched with Provider.of<T>(context, listen: true) to register the Widget control.
      • There is too much logic involved, all of which are in the above Provider source code analysis article, if you are interested, you can go and see
  • BlocProvider.of<T>

    • Function: You can get the XxxBloc passed in by BlocProvider Create in the child control wrapped by BlocProvider
    • Please note: If the parent layout context of BlocProvider is used, XxxBloc cannot be obtained, it must be a sub-layout of BlocProvider
    • Principle: Source code: The other side of Flutter Provider (4D text + plug-in) , still in this article 
      • I really don't want to promote this article. In the BlocProvider part, Bloc uses too many Provider features.
      • Provider article, I spent a lot of effort to analyze the principle, here, there is no need to make a repeater

Summary: To summarize the role of the BlocProvider class

  1. BlocProvider may store externally passed XxxBloc instances, XxxBloc classes must inherit BlocBase
  2. The XxxBloc instance stored by BlocProvider can be obtained through BlocProvider.of<T> (must be in BlocProvider or its sub-Widget)
  3. The instance XxxBloc obtained by BlocProvider can be automatically released; the XxxBloc of the BlocProvider.value named constructor instance will not be automatically released

BlocProvider实现了上面这三个碉堡的功能,基本就可以把Stream使用模式彻底精简了

  • icon

Cornerstone BlocBase

Undoubtedly, BlocBase is an important abstract class

  • BlocBase
abstract class BlocBase<State> {
  BlocBase(this._state) {
    Bloc.observer.onCreate(this);
  }

  StreamController<State>? __stateController;
  StreamController<State> get _stateController {
    return __stateController ??= StreamController<State>.broadcast();
  }

  State _state;

  bool _emitted = false;

  State get state => _state;

  Stream<State> get stream => _stateController.stream;

  @Deprecated(
    'Use stream.listen instead. Will be removed in v8.0.0',
  )
  StreamSubscription<State> listen(
    void Function(State)? onData, {
    Function? onError,
    void Function()? onDone,
    bool? cancelOnError,
  }) {
    return stream.listen(
      onData,
      onError: onError,
      onDone: onDone,
      cancelOnError: cancelOnError,
    );
  }

  void emit(State state) {
    if (_stateController.isClosed) return;
    if (state == _state && _emitted) return;
    onChange(Change<State>(currentState: this.state, nextState: state));
    _state = state;
    _stateController.add(_state);
    _emitted = true;
  }

  @mustCallSuper
  void onChange(Change<State> change) {
    Bloc.observer.onChange(this, change);
  }

  @mustCallSuper
  void addError(Object error, [StackTrace? stackTrace]) {
    onError(error, stackTrace ?? StackTrace.current);
  }

  @protected
  @mustCallSuper
  void onError(Object error, StackTrace stackTrace) {
    Bloc.observer.onError(this, error, stackTrace);
    assert(() {
      throw BlocUnhandledErrorException(this, error, stackTrace);
    }());
  }

  @mustCallSuper
  Future<void> close() async {
    Bloc.observer.onClose(this);
    await _stateController.close();
  }
}

The above BlocBase has done several important things to sort out

Bloc.observer is not important, this is a class defined inside the framework, it can be ignored here, it is not very important

  1. Stores the incoming state object 
    • Every time you use emit to refresh, the incoming state will be replaced by the previously stored state object
    • emit makes a judgment, if the incoming state and the stored state object are the same, the refresh operation will not be performed (this is why I added the clone method in the State class)
  2. Initializes a series of Stream objects
  3. Encapsulates the operation of closing the Stream stream
  • Simplify the above code
abstract class BlocBase<T> {
  BlocBase(this.state) : _stateController = StreamController<T>.broadcast();

  final StreamController<T> _stateController;

  T state;

  bool _emitted = false;

  Stream<T> get stream => _stateController.stream;

  void emit(T newState) {
    if (_stateController.isClosed) return;
    if (state == newState && _emitted) return;
    state = newState;
    _stateController.add(state);
    _emitted = true;
  }

  @mustCallSuper
  Future<void> close() async {
    await _stateController.close();
  }
}

BlocBuilder

BlocBuilder has simplified the usage of StreamBuilder a lot, let's take a look at the internal implementation

  • BlocBuilder 
    • Here you need to pay attention to the builder parameter; buildWhen is a parameter to judge whether it needs to be updated
    • The builder is called in the build method , you need to look at the parent class BlocBuilderBase
typedef BlocWidgetBuilder<S> = Widget Function(BuildContext context, S state);

class BlocBuilder<B extends BlocBase<S>, S> extends BlocBuilderBase<B, S> {
  const BlocBuilder({
    Key? key,
    required this.builder,
    B? bloc,
    BlocBuilderCondition<S>? buildWhen,
  }) : super(key: key, bloc: bloc, buildWhen: buildWhen);

  final BlocWidgetBuilder<S> builder;

  @override
  Widget build(BuildContext context, S state) => builder(context, state);
}
  • BlocBuilderBase 
    • context.read<B>() and Provider.of<T>(this, listen: false) have the same effect, which is an encapsulation of the latter
    • Here, we get the XxxBloc object we passed in BlocProvider through context.read<B>() and assign it to the _bloc variable in _BlocBuilderBaseState
    • BlocBuilderBase abstracts a build method and assigns it to BlocListener in _BlocBuilderBaseState
    • BlocBuilderBase still can't see the refresh logic, several important parameters: _bloc, listener, widget.build are all passed to BlocListener; need to see the implementation of BlocListener
abstract class BlocBuilderBase<B extends BlocBase<S>, S>
    extends StatefulWidget {
  const BlocBuilderBase({Key? key, this.bloc, this.buildWhen})
      : super(key: key);

  final B? bloc;

  final BlocBuilderCondition<S>? buildWhen;

  Widget build(BuildContext context, S state);

  @override
  State<BlocBuilderBase<B, S>> createState() => _BlocBuilderBaseState<B, S>();
}

class _BlocBuilderBaseState<B extends BlocBase<S>, S>
    extends State<BlocBuilderBase<B, S>> {
  late B _bloc;
  late S _state;

  @override
  void initState() {
    super.initState();
    _bloc = widget.bloc ?? context.read<B>();
    _state = _bloc.state;
  }

  ...

  @override
  Widget build(BuildContext context) {
    ...
    return BlocListener<B, S>(
      bloc: _bloc,
      listenWhen: widget.buildWhen,
      listener: (context, state) => setState(() => _state = state),
      child: widget.build(context, _state),
    );
  }
}
  • BlocListener: The parameters are passed to the constructor of the parent class, you need to look at the implementation of the parent class BlocListenerBase
class BlocListener<B extends BlocBase<S>, S> extends BlocListenerBase<B, S>
  const BlocListener({
    Key? key,
    required BlocWidgetListener<S> listener,
    B? bloc,
    BlocListenerCondition<S>? listenWhen,
    Widget? child,
  }) : super(
          key: key,
          child: child,
          listener: listener,
          bloc: bloc,
          listenWhen: listenWhen,
        );
}
  • BlocListenerBase: Simplified some logic code
abstract class BlocListenerBase<B extends BlocBase<S>, S>
    extends SingleChildStatefulWidget {
  const BlocListenerBase({
    Key? key,
    required this.listener,
    this.bloc,
    this.child,
    this.listenWhen,
  }) : super(key: key, child: child);

  final Widget? child;

  final B? bloc;

  final BlocWidgetListener<S> listener;

  final BlocListenerCondition<S>? listenWhen;

  @override
  SingleChildState<BlocListenerBase<B, S>> createState() =>
      _BlocListenerBaseState<B, S>();
}

class _BlocListenerBaseState<B extends BlocBase<S>, S>
    extends SingleChildState<BlocListenerBase<B, S>> {
  StreamSubscription<S>? _subscription;
  late B _bloc;
  late S _previousState;

  @override
  void initState() {
    super.initState();
    _bloc = widget.bloc ?? context.read<B>();
    _previousState = _bloc.state;
    _subscribe();
  }

  ...

  @override
  Widget buildWithChild(BuildContext context, Widget? child) {
    return child!;
  }

  @override
  void dispose() {
    _unsubscribe();
    super.dispose();
  }

  void _subscribe() {
    _subscription = _bloc.stream.listen((state) {
      if (widget.listenWhen?.call(_previousState, state) ?? true) {
        widget.listener(context, state);
      }
      _previousState = state;
    });
  }

  void _unsubscribe() {
    _subscription?.cancel();
    _subscription = null;
  }
}

Finally found the key code!

It can be found that Bloc is refreshed through the cooperation of StreamController and listen

Called widget.listener(context, state), the implementation method is setState, you can look at the _BlocBuilderBaseState class

_bloc.stream.listen(
  (state) {
    if (widget.listenWhen?.call(_previousState, state) ?? true) {
      widget.listener(context, state);
    }
    _previousState = state;
  },
);

Streamlined BlocBuild

The implementation logic of the above BlocBuild is still too convoluted, and there are too many encapsulation levels. Let's write a simplified version of BlocBuild.

Of course, the core logic of BlocBuild refresh will definitely be retained

class BlocEasyBuilder<T extends BlocBase<V>, V> extends StatefulWidget {
  const BlocEasyBuilder({
    Key? key,
    required this.builder,
  }) : super(key: key);

  final Function(BuildContext context, V state) builder;

  @override
  _BlocEasyBuilderState createState() => _BlocEasyBuilderState<T, V>();
}

class _BlocEasyBuilderState<T extends BlocBase<V>, V>
    extends State<BlocEasyBuilder<T, V>> {
  late T _bloc;
  late V _state;
  StreamSubscription<V>? _listen;

  @override
  void initState() {
    _bloc = BlocProvider.of<T>(context);
    _state = _bloc.state;

    //数据改变刷新Widget
    _listen = _bloc.stream.listen((event) {
      setState(() {});
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return widget.builder(context, _state);
  }

  @override
  void dispose() {
    _listen?.cancel();
    super.dispose();
  }
}
  • Take a look at the renderings: For detailed usage code, please check: flutter_use

Event mechanism

If you use the Bloc mode to develop, there will be an additional Event layer, which defines all event interactions

mention it here

  • Bloc: some code omitted
abstract class Bloc<Event, State> extends BlocBase<State> {
  /// {@macro bloc}
  Bloc(State initialState) : super(initialState) {
    _bindEventsToStates();
  }

  StreamSubscription<Transition<Event, State>>? _transitionSubscription;

  StreamController<Event>? __eventController;
  StreamController<Event> get _eventController {
    return __eventController ??= StreamController<Event>.broadcast();
  }

  void add(Event event) {
    if (_eventController.isClosed) return;
    try {
      onEvent(event);
      _eventController.add(event);
    } catch (error, stackTrace) {
      onError(error, stackTrace);
    }
  }

  Stream<Transition<Event, State>> transformEvents(
    Stream<Event> events,
    TransitionFunction<Event, State> transitionFn,
  ) {
    return events.asyncExpand(transitionFn);
  }

  @protected
  @visibleForTesting
  @override
  void emit(State state) => super.emit(state);

  Stream<State> mapEventToState(Event event);

  Stream<Transition<Event, State>> transformTransitions(
    Stream<Transition<Event, State>> transitions,
  ) {
    return transitions;
  }

  @override
  @mustCallSuper
  Future<void> close() async {
    await _eventController.close();
    await _transitionSubscription?.cancel();
    return super.close();
  }

  void _bindEventsToStates() {
    _transitionSubscription = transformTransitions(
      transformEvents(
        _eventController.stream,
        (event) => mapEventToState(event).map(
          (nextState) => Transition(
            currentState: state,
            event: event,
            nextState: nextState,
          ),
        ),
      ),
    ).listen(
      (transition) {
        if (transition.nextState == state && _emitted) return;
        try {
          emit(transition.nextState);
        } catch (error, stackTrace) {
          onError(error, stackTrace);
        }
      },
      onError: onError,
    );
  }
}

The overall logic is relatively clear, let's take a look

  1. Bloc is an abstract class 
    • Call the _bindEventsToStates() method in the constructor
    • Bloc abstracts a mapEventToState(Event event) method, inherits the Bloc abstract class, and must implement this method
  2. In the Bloc class, the Stream object is instantiated to do the event triggering mechanism of the Event 
    • When an Event event is added, the listener callback in the _bindEventsToStates() method is triggered
  3. Some operations are done in _bindEventsToStates 
    • Added Event: events.asyncExpand(transitionFn); first pass its own Event parameter into the transitionFn method for execution
    • The logic of transitionFn is: pass the Event parameter into mapEventToState, and then mapEventToState returns the State object
    • Then trigger the listen callback, in listen, pass the state to emit, and then trigger the refresh control to rebuild

Summarize

After the analysis of the above key classes, the operation mechanism of the entire Bloc becomes clear at once

BlocProvider

  • Responsible for storing incoming XxxBloc for storage

  • The provided of method can obtain the stored XxxBloc at the location of the BlocProvider or its child nodes

  • Provides a callback for recycling resources (recycling Stream)

BlocBase

  • Stores the incoming state object

  • Initializes a series of Stream objects

  • Encapsulates the operation of closing the Stream stream

BlocBuilder

  • The essence is StatefulWidget
  • Obtain XxxBloc through BlocProvider, and then monitor data changes through its listener method
  • After the data changes, rebuild the StatefulWidget through setState to achieve the effect of partial refresh

Hand rubbing a state management framework

The principle of Bloc is much simpler than that of Provider. . .

Imitation of Bloc's refresh mechanism, come and rub a state management framework! Name it EasyC!

rub hands

  • EasyC: First, you need to write a base class to handle a series of operations on Stream
abstract class EasyC<T> {
  EasyC(this.state) : _controller = StreamController<T>.broadcast();

  final StreamController<T> _controller;

  T state;

  bool _emitted = false;

  Stream<T> get stream => _controller.stream;

  void emit(T newState) {
    if (_controller.isClosed) return;
    if (state == newState && _emitted) return;
    state = newState;
    _controller.add(state);
    _emitted = true;
  }

  @mustCallSuper
  Future<void> close() async {
    await _controller.close();
  }
}
  • EasyCProvider 
    • The InheritedProvider provided by the Provider framework is not used here.
    • Here I rubbed one with InheritedWidget
    • The of method and the closing of the stream are all done; there is no need to manually close the stream, and there is no need to write a StatefulWidget!
class EasyCProvider<T extends EasyC> extends InheritedWidget {
  EasyCProvider({
    Key? key,
    Widget? child,
    required this.create,
  }) : super(key: key, child: child ?? Container());

  final T Function(BuildContext context) create;

  @override
  bool updateShouldNotify(InheritedWidget oldWidget) => false;

  @override
  InheritedElement createElement() => EasyCInheritedElement(this);

  static T of<T extends EasyC>(BuildContext context) {
    var inheritedElement =
        context.getElementForInheritedWidgetOfExactType<EasyCProvider<T>>()
            as EasyCInheritedElement<T>?;

    if (inheritedElement == null) {
      throw 'not found';
    }

    return inheritedElement.value;
  }
}

class EasyCInheritedElement<T extends EasyC> extends InheritedElement {
  EasyCInheritedElement(EasyCProvider<T> widget) : super(widget);

  bool _firstBuild = true;

  late T _value;

  T get value => _value;

  @override
  void performRebuild() {
    if (_firstBuild) {
      _firstBuild = false;
      _value = (widget as EasyCProvider<T>).create(this);
    }

    super.performRebuild();
  }

  @override
  void unmount() {
    _value.close();
    super.unmount();
  }
}
  • EasyCBuilder: The last whole fixed-point refresh Widget
class EasyCBuilder<T extends EasyC<V>, V> extends StatefulWidget {
  const EasyCBuilder({
    Key? key,
    required this.builder,
  }) : super(key: key);

  final Function(BuildContext context, V state) builder;

  @override
  _EasyCBuilderState createState() => _EasyCBuilderState<T, V>();
}

class _EasyCBuilderState<T extends EasyC<V>, V>
    extends State<EasyCBuilder<T, V>> {
  late T _easyC;
  late V _state;
  StreamSubscription<V>? _listen;

  @override
  void initState() {
    _easyC = EasyCProvider.of<T>(context);
    _state = _easyC.state;

    //数据改变刷新Widget
    _listen = _easyC.stream.listen((event) {
      setState(() {});
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return widget.builder(context, _state);
  }

  @override
  void dispose() {
    _listen?.cancel();
    super.dispose();
  }
}

The above three files basically reproduce the refresh mechanism of Bloc

At the same time, it also removed a knot in my heart, the Bloc source code used the _startListening method of the Provider inexplicably.

use

The use is basically the same as Bloc

I originally wanted to remove the judgment of the comparison between the two new and old state objects of emit, but I think that the author of Bloc seems to have a deep obsession with this concept and has dealt with it in many places; therefore, I will keep it here. The original usage of Bloc can be preserved

  • view
class CounterEasyCPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return EasyCProvider(
      create: (BuildContext context) => CounterEasyC(),
      child: Builder(builder: (context) => _buildPage(context)),
    );
  }

  Widget _buildPage(BuildContext context) {
    final easyC = EasyCProvider.of<CounterEasyC>(context);

    return Scaffold(
      appBar: AppBar(title: Text('自定义状态管理框架-EasyC范例')),
      body: Center(
        child: EasyCBuilder<CounterEasyC, CounterEasyCState>(
          builder: (context, state) {
            return Text(
              '点击了 ${easyC.state.count} 次',
              style: TextStyle(fontSize: 30.0),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => easyC.increment(),
        child: Icon(Icons.add),
      ),
    );
  }
}
  • logic
class CounterEasyC extends EasyC<CounterEasyCState> {
  CounterEasyC() : super(CounterEasyCState().init());

  ///自增
  void increment() => emit(state.clone()..count = ++state.count);
}
  • state
class CounterEasyCState {
  late int count;

  CounterEasyCState init() {
    return CounterEasyCState()..count = 0;
  }

  CounterEasyCState clone() {
    return CounterEasyCState()..count = count;
  }
}
  • renderings

Global is also possible, it is no different from Provider, I will not repeat it here.

Summarize

This hand-rubbed EasyC frame retains the essence of the Bloc refresh mechanism, and at the same time, it has also done a lot of simplification

I believe that people who are destined can understand as long as they look carefully.

The source code of Bloc is not complicated. It uses Stream and has made a big simplification. The basic pain points are all encapsulated and processed internally.

finally

Choosing a state management framework should be a more prudent thing; you can first look at its principles and understand its internal operating mechanism, and then you can choose as needed, because you understand its internal operating mechanism, even if If you have any problems during use, you can deal with them calmly; if you are afraid that the author will abandon the pit or are not satisfied with its functions, choose the refresh mechanism you want and rub one yourself!

I have written corresponding plugins for the three frameworks Provider, Bloc and GetX. If the state management framework you choose is any of these three frameworks, I believe these plugins can help you complete some repetitive workloads.

Related address

  • The Github address of the Demo in the article: flutter_use

 

Guess you like

Origin blog.csdn.net/jdsjlzx/article/details/123432110