Skip to content

[Proposal] Replace mapEventToState with on<Event> in Bloc #2526

Description

@felangel

Hello everyone! 👋

First of all, I want to thank everyone for the amazing support and community that has grown around the bloc library! 🙏💙

Context

This proposal aims to address 3 problems with the current mapEventToState implementation:

  1. Predictability
  2. Learning Curve and Complexity
  3. Boilerplate

Predictability

Due to an issue in Dart, it is not always intuitive what the value of state will be when dealing with nested async generators which emit multiple states. Even though there are ways to work around this issue, one of the core principles/goals of the bloc library is to be predictable. Therefore, the primary motivation of this proposal is to make the library as safe as possible to use and eliminate any uncertainty when it comes to the order and value of state changes.

Learning Curve and Complexity

Writing blocs requires an understanding of Streams and async generators. This means developers must understand how to use the async*, yield, and yield* keywords. While these concepts are covered in the documentation, they are still fairly complex and difficult for newcomers to grasp.

Boilerplate

When writing a bloc, developers must override mapEventToState and then handle the incoming event(s). Often times this looks something like:

@override
Stream<State> mapEventToState(Event event) async* {
  if (event is EventA) {
    yield* _mapEventAToState(event);
  } else if (event is EventB) {
    yield* _mapEventBToState(event);
  }
}

Stream<State> _mapEventAToState(EventA event) async* {...}
Stream<State> _mapEventBToState(EventB event) async* {...}

The important logic usually lives inside _mapEventAToState and _mapEventBToState and mapEventToState ends up mainly being setup code to handle determining which mapper to call based on the event type. It would be nice if this could be streamlined.

Proposal 🥁

I am proposing to remove the mapEventToState API in favor of on<Event>. This would allow developers to register event handlers by calling on<Event> where Event is the type of event being handled. on<Event> would provide a callback (Event event, Emitter<State>) {...} which would be invoked when an event of type Event is added to the bloc. Developers could then emit one or more states in response to the incoming event.

For example, if we look at the CounterBloc for reference, the current implementation might look something like:

abstract class CounterEvent {}
class Increment extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0);

  @override
  Stream<int> mapEventToState(CounterEvent event) async* {
    if (event is Increment) {
      yield state + 1;
    }
  }
}

With the proposed changes the CounterBloc would look something like:

abstract class CounterEvent {}
class Increment extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>((event, emit) => emit(state + 1));
  }
}

If we wanted to support multiple events:

abstract class CounterEvent {}
class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>((event, emit) => emit(state + 1));
    on<Decrement>((event, emit) => emit(state - 1));
  }
}

For more complex logic it can be refactored to look like:

abstract class CounterEvent {}
class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>(_onIncrement);
    on<Decrement>(_onDecrement);
  }

  void _onIncrement(Increment event, Emitter<int> emit) {
    emit(state + 1);
  }

  void _onDecrement(Decrement event, Emitter<int> emit) {
    emit(state - 1);
  }
}

These changes address the predictability issues mentioned above because it can be guaranteed that the bloc's state will update immediately when emit is called ensuring that cases like this behave as expected:

  void _onIncrement(Increment event, Emitter<int> emit) {
    emit(state + 1); // state starts off at 0 so we emit 1
    emit(state + 1); // state is 1 now so we emit 2
  }

In addition, developers don't have to use async generators (async*, yield, yield*) which can introduce complexity and undesired behavior in advanced cases.

This allows developers to focus on the logic by directly registering an event handler for each type of event which streamlines the bloc code a bit further.

An added benefit is the added consistency across Cubit and Bloc -- both trigger state changes via emit and the transition from Cubit to Bloc should become simpler.

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
}

Becomes

abstract class CounterEvent {}
class Increment extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>(_onIncrement);
  }

  void _onIncrement(Increment event, Emitter<int> emit) {
    emit(state + 1);
  }
}

Or as mentioned above (for simple cases)

abstract class CounterEvent {}
class Increment extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>((event, emit) => emit(state + 1));
  }
}

These changes will obviously be breaking changes that impact blocs (cubits will remain unaffected) so they would all be within the scope of a v8.0.0 release.

The changes would be made in a way that only impacts the bloc mapEventToState implementation. The way blocs are used and tested will be 100% backward compatible which means the changes will be scoped to just the mapEventToState code and can ideally be automated via a code mod. There should be no impact to the remaining ecosystem (flutter_bloc, bloc_test, hydrated_bloc, replay_bloc, etc...).

Please give this issue a 👍 if you support the proposal or a 👎 if you're against it. If you disagree with the proposal I would really appreciate it if you could comment with your reasoning.

Thanks so much for all of the continued support and looking forward to hearing everyone's thoughts on the proposal! 🙏


08/31 UPDATE

Hey everyone, just wanted to give a quick update:

We currently have the v8.0.0 branch which replaces mapEventToState with on<Event>; however, we were able to make on<Event> backward compatible with mapEventToState 🎉 . You can view the changes as part of the v7.2.0 branch.

The current plan is to roll out bloc v7.2.0 in the coming days which will deprecate mapEventToState, transformEvents, and transformTransitions and will introduce the new on<Event> API. We will have a comprehensive migration guide explaining all of the changes and how to migrate over. During this time, we encourage everyone to upgrade to bloc v7.2.0 and start to migrate blocs one at a time. In the meantime, we'll be working on development releases of bloc v8.0.0.

As part of v8.0.0 all deprecated APIs from v7.2.0 will be removed and the tentative plan is to publish a stable v8.0.0 release about a month after v7.2.0 has been release. This should give everyone some time to incrementally migrate and for any adjustments to be made. In addition, v7.x.x will still receive bug fixes for the foreseeable future so there should be no pressure/urgency to jump to v8.0.0.

Let us know if you have any questions/concerns.

Thanks for everyone's feedback, patience, and continued support! 💙 🙏

09/02 UPDATE

We just published bloc v7.2.0-dev.1 which introduces the on<Event> API and is backwards compatible which should allow you to migrate incrementally. 🎉 ✨

bloc-v7 2 0

Release Notes: https://github.com/felangel/bloc/releases/tag/bloc-v7.2.0-dev.1

Please try it out and let us know if you have any feedback/questions, thanks! 🙏 💙

09/09 UPDATE

We just published bloc v7.2.0-dev.2 🎉
Release Notes: https://github.com/felangel/bloc/releases/tag/bloc-v7.2.0-dev.2

09/10 UPDATE

We just published bloc v7.2.0-dev.3 🎉
Release Notes: https://github.com/felangel/bloc/releases/tag/bloc-v7.2.0-dev.3

09/21 UPDATE

The time has come 🥁 🥁 🥁

bloc v7.2.0 is now out 🎉

📦 update now: https://pub.dev/packages/bloc/versions/7.2.0
📔 migration guide: https://bloclibrary.dev/#/migration?id=v720

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

discussionOpen discussion for a specific topicenhancement candidateCandidate for enhancement but additional research is neededfeedback wantedLooking for feedback from the communitypkg:blocThis issue is related to the bloc package

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions