felangel / bloc

A predictable state management library that helps implement the BLoC design pattern
https://bloclibrary.dev
MIT License
11.71k stars 3.38k forks source link

[proposal] feat(bloc): add mixins for common operations #3084

Open purplenoodlesoop opened 2 years ago

purplenoodlesoop commented 2 years ago

Hi @felangel!

Description

There are a few common patterns that are used across Blocs and Cubits that require quite a few lines of boilerplate code and are error-prone.

They are related, but are not limited to: Bloc-to-Bloc/Cubit-to-Cubit/any other combination communications, Repository consumption (sending an event when a new entity is emitted by Repository), logging, and more.

Those patterns follow the same sequence of actions:

There are three main cases with that behavior:

1) Listening to a stream to perform any side effects 2) Reacting to stream with events (a subset of the first case) 3) Performing cancelable async operations (#3069)

Desired Solution

An elegant solution to that problem can be implemented through the help of mixins. For each case, the own mixin can be created.

1) BlocListeningMixin on Closable minimally containing listenToStream and listenToStreamable methods. 2) BlocReactingMixin on BlocListeningMixin and BlocEventSink minimally containing reactToStream and reactToStreamable methods. 3) BlocAsyncMixin on Closable minimally containing cancelable method.

Of course, those methods are only minimal content.

Alternatives Considered

An alternative will be to not change anything and keep track of cancelable objects by hand. That approach certainly works, but requires more code, is more error-prone, and requires more mental load on the developer.

Additional Context

A sample and rough implementation of BlocListeningMixin would look something like that. Note, that methods return an instance of StreamSubscription for cases when it should be canceled manually before the closing of the Closable.

mixin BlocListeningMixin on Closable {
  final List<StreamSubscription<dynamic>> _subscriptions = [];

  @protected
  StreamSubscription<T> listenToStream<T>(
    Stream<T> stream,
    void Function(T event) subscriber,
  ) {
    final subscription = stream.listen(subscriber);
    _subscriptions.add(subscription);
    return subscription;
  }

  @protected
  StreamSubscription<T> listenToStreamable<T>(
    Streamable<T> streamable,
    void Function(T event) subscriber,
  ) =>
      listenToStream(streamable.stream, subscriber);

  @override
  Future<void> close() async {
    for (final subscription in _subscriptions) {
      await subscription.cancel();
    }
    return super.close();
  }
}
felangel commented 2 years ago

Hi @purplenoodlesoop 👋 Thanks for opening an issue!

When using bloc you can make use of emit.forEach and emit.onEach to handle reacting to an incoming stream. The subscription is automatically managed for you and you don't have to worry about cancelations. I'll think about this proposal a bit more but I'm not convinced that this would add a lot of value because while there will be fewer lines of code written, the code will be less explicit/clear.

purplenoodlesoop commented 2 years ago

@felangel thanks for the response!

When using bloc you can make use of emit.forEach and emit.onEach to handle reacting to an incoming stream.

This is certainly very true, but that capability is limited to Blocs only – mixins would allow adding that automatic subscription management to any Closable – a Cubit or even a custom implementation of the interface.

...this would add a lot of value because while there will be fewer lines of code written, the code will be less explicit/clear.

I also agree with the fact that it will make code a bit more "magical", but besides cutting the amount of code significantly, removing 2 out of 3 steps from the whole pipeline, mixins will make code less error-prone and further help with reducing the learning curve – it is possible to forget to cancel a StreamSubscription or a CancelableOperation.

felangel commented 2 years ago

This is certainly very true, but that capability is limited to Blocs only – mixins would allow adding that automatic subscription management to any Closable – a Cubit or even a custom implementation of the interface.

Yup definitely agree it would be especially useful for Cubits.

I also agree with the fact that it will make code a bit more "magical", but besides cutting the amount of code significantly, removing 2 out of 3 steps from the whole pipeline, mixins will make code less error-prone and further help with reducing the learning curve – it is possible to forget to cancel a StreamSubscription or a CancelableOperation.

Yeah definitely agree. I'll leave this open for a bit and try to get more feedback from the community to inform the decision.

Thanks again! 💙

Gene-Dana commented 2 years ago

They are related, but are not limited to: Bloc-to-Bloc/Cubit-to-Cubit/any other combination communications,

Hi there ! This is really something that is highly discouraged due to the declarative nature of the api

purplenoodlesoop commented 2 years ago

Hi @Gene-Dana!

Hi there ! This is really something that is highly discouraged due to the declarative nature of the API

Sorry, but I think we are talking about different things. Bloc-to-Bloc communication is not discouraged, does not breaks declarativity and there is a whole section in documentation dedicated to it.

alestiago commented 2 years ago

Hi @purplenoodlesoop ! Thanks for this issue.

A major disadvantage is that including mixins might increase the complexity of the library significantly for newcomers. I believe it is easier to conceptualise a method such as emit.OnEach and emit.ForEach than a mixin for new developers. I do agree with @felangel , making use of mixins can make the code "less explicit/clear".

However, having said this, I think the methods that the Emitter exposes can be useful for Cubit. Although, in practice, I always tend to rely on Bloc rather than Cubit unless there is very low complexity, and I consider listening to a stream complex enough to use a Bloc.

Hence, I think we can ask ourselves if we want Cubit to have access to methods similar to emit.OnEach and emit.ForEach? If so, perhaps a mixin might be a possible solution. Although it comes with some caveats.

Regarding the BlocAsyncMixin I think this is very interesting. Since discussing the similarities of Bloc to Automata theory in #3162 , I've been thinking that it might be a good idea to have a concept of a FinalState. In other words, a state that when reached terminates the bloc (can not emit new states). One could handle when a FinalState is reached and close the resources that are no longer required. I wonder if this might be applicable or of any help to the idea of the cancellable method.

I hope this feedback helps.