Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
felangel opened this issue Jun 13, 2021 · 135 comments · Fixed by #2702
Closed

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

felangel opened this issue Jun 13, 2021 · 135 comments · Fixed by #2702
Assignees
Labels
discussion Open discussion for a specific topic enhancement candidate Candidate for enhancement but additional research is needed feedback wanted Looking for feedback from the community pkg:bloc This issue is related to the bloc package

Comments

@felangel
Copy link
Owner

felangel commented Jun 13, 2021

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

@felangel felangel added discussion Open discussion for a specific topic feedback wanted Looking for feedback from the community pkg:bloc This issue is related to the bloc package breaking change Enhancement candidate would introduce a breaking change labels Jun 13, 2021
@felangel felangel self-assigned this Jun 13, 2021
@felangel felangel pinned this issue Jun 13, 2021
@rishabhdeepsingh
Copy link

I Like this one <3

@Gene-Dana
Copy link
Collaborator

Gene-Dana commented Jun 13, 2021

With the understanding that this will not impact everything that's already in place, meaning this change will not require a refactor in the presentation or the domain layer, this is epic.

To me, simplicity is elegance and this makes a lot of sense !

Instead of

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

  Stream<int> _mapIncrementToState(Increment event) async* {
    yield state + 1;
  }

we have

   on<Increment>((event, emit) => emit(state + 1));

And to me that is infinitely more elegant

@felangel felangel added the enhancement candidate Candidate for enhancement but additional research is needed label Jun 14, 2021
@jolexxa
Copy link
Collaborator

jolexxa commented Jun 14, 2021

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.

That was definitely the biggest pain point while learning Dart, Flutter, and Bloc at the same time. I would often get confused as to when to use async vs async* and yield vs yield*.

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

One of the cool things about bloc is how native it felt by encouraging the use of Dart's built in generator features (despite the learning curve). I still think this is a good change, though, given how much it reduces the amount of boilerplate needed and the fact that Dart still hasn't released the fix for such a critical bug.

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.

That is actually the biggest benefit of adopting this approach, to me. It feels much more declarative, cleaner, and seems to fit nicely within bloc's approach to state management.

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));
  }
}

That cleaned up nicely 😮

@AbhishekDoshi26
Copy link

AbhishekDoshi26 commented Jun 14, 2021

This looks clean and awesome. Just a doubt, will this be creating a subscription or listener kinda thing when the object of bloc is created?
Eg: CounterBloc _counterBloc = BlocProvider.of(context); //Will this create a listener or subscription to:

 CounterBloc() : super(0) {
    on<Increment>(_onIncrement);
    on<Decrement>(_onDecrement);
  }

as there won't be any method now.

@felangel
Copy link
Owner Author

This looks clean and awesome. Just a doubt, will this be creating a subscription or listener kinda thing when the object of bloc is created?
Eg: CounterBloc _counterBloc = BlocProvider.of(context); //Will this create a listener or subscription to:

 CounterBloc() : super(0) {
    on<Increment>(_onIncrement);
    on<Decrement>(_onDecrement);
  }

as there won't be any method now.

Thanks for the feedback! It shouldn't create any additional subscriptions -- it'll just register the handler for that event type and under the hood the number of subscriptions should stay the same. Hope that helps 👍

@prakash-O4
Copy link

That was pretty helpful and easy to understand

@mrverdant13
Copy link

Really nice and semantic refactoring! 🚀
(Feels like C/C++ enum-based-event handling)

Any way to unregister or update an event handler? 🤔
It would add more flexibility for cases when a cubit/bloc behavior might change dynamically in the run.

@cmc5788
Copy link
Contributor

cmc5788 commented Jun 14, 2021

What would an example that includes some basic async processing look like?

@felangel
Copy link
Owner Author

Really nice and semantic refactoring! 🚀
(Feels like C/C++ enum-based-event handling)

Any way to unregister or update an event handler? 🤔
It would add more flexibility for cases when a cubit/bloc behavior might change dynamically in the run.

Thanks! I thought about having on returning something similar to a subscription so you could potentially dynamically register and unregister handlers but unless there's a compelling use case I'd prefer to keep it simple.

@felangel
Copy link
Owner Author

What would an example that includes some basic async processing look like?

It could look something like:

abstract class WeatherEvent {}
class WeatherRequested extends WeatherEvent {
  WeatherRequested(this.latLng);
  final LatLng latLng;
}

abstract class WeatherState {}
class WeatherInitial extends WeatherState {}
class WeatherLoadInProgress extends WeatherState {}
class WeatherLoadFailure extends WeatherState {}
class WeatherLoadSuccess extends WeatherState {
  WeatherLoadSuccess(this.weather);
  final Weather weather;
}


class WeatherBloc extends Bloc<WeatherEvent, WeatherState> {
  WeatherBloc(this.weatherRepository) : super(WeatherInitial()) {
    on<WeatherRequested>(_onWeatherRequested);
  }

  final WeatherRepository weatherRepository;

  void _onWeatherRequested(WeatherRequested event, Emit<WeatherState> emit) async {
    emit(WeatherLoadInProgress());
    try {
      final weather = await weatherRepository.getWeather(event.latLng);
      emit(WeatherLoadSuccess(weather);
    } catch (_) {
      emit(WeatherLoadFailure());
    }
  }
}

Hope that helps 👍

@cmc5788
Copy link
Contributor

cmc5788 commented Jun 14, 2021

Gotcha, so the idea is that on<T> registrars would accept async or async* functions still to manage arbitrarily complex event handlers?

@felangel
Copy link
Owner Author

Gotcha, so the idea is that on<T> registrars would accept async or async* functions still to manage arbitrarily complex event handlers?

Yeah on<T> would allow you to register an event handler for events of type T which could vary in complexity 👍

@TheWCKD
Copy link

TheWCKD commented Jun 14, 2021

Amazing update and a pure example on how minimalism, even though it's tough to achieve, can be such a fresh breath of air for bloc and not only that! Thanks, @felangel!

@amr-eniou-3r
Copy link

amr-eniou-3r commented Jun 14, 2021

@felangel It is very clean, I would like to see a similar way to the Freezed library

state.When/MaybeWhen and state.Map/MaybeMap
and of course state.copyWith too

@felangel
Copy link
Owner Author

felangel commented Jun 14, 2021

@felangel It is very clean, I would like to see a similar way to the Freezed library

state.When/MaybeWhen and state.Map/MaybeMap
and of course state.copyWith too

Thanks! You should still be able to use freezed with when, copyWith, etc... with this approach

class MyBloc extends Bloc<MyEvent, MyState> {
  MyBloc() : super(MyState.initial()) {
    on<MyEvent>(_onEvent);
  }

  void _onEvent(MyEvent event, Emit<MyState> emit) async {
    event.when(
      a: () => emit(state.copyWith(...)),
      b: () => emit(state.copyWith(...)),
      ...
  }
}

@amr-eniou-3r
Copy link

amr-eniou-3r commented Jun 14, 2021

@felangel I mean merge freezed magic to your magic 😄 , no problem we can still use it separately

@jorgecoca
Copy link
Collaborator

This comment will add zero value to the proposal (which, by the way, amazing job on making the message clear!)... but I can't wait to see sealed classes/unions and data classes baked into Dart! These API changes + those language changes will be 💯!

@elias8
Copy link

elias8 commented Jun 14, 2021

Nice. That will make the code less boilerplate and more readable at the same time for bloc.

This will be like reading plain English!

on<Increment>(_onIncrement);
on<Decrement>(_onDecrement);

But I see that the cubit code is changed to a bloc by having an event instead of public methods

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, Emit<int> emit) {
    emit(state + 1);
  }
}

So how is that not going to break cubits?

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.

Or do I miss something?

Thanks! Sorry for the confusion, the cubit code will not change at all. I was just trying to show how these changes would make it easier to convert a cubit into a bloc at a later time if you decided you wanted to switch. Let me know if that helps 👍

@elias8
Copy link

elias8 commented Jun 14, 2021

Understood 👍

@adrianflutur
Copy link

That's great 👌
Can we also maybe rename the Emit class to Emitter? Honestly Emit sounds more like a verb, like an action to be done (e.g. 'change', 'set'), not really something which provides me with that action (e.g. 'changer', 'setter') 😁

@felangel
Copy link
Owner Author

felangel commented Jun 14, 2021

That's great 👌
Can we also maybe rename the Emit class to Emitter? Honestly Emit sounds more like a verb, like an action to be done (e.g. 'change', 'set'), not really something which provides me with that action (e.g. 'changer', 'setter') 😁

Thanks! Emit would just be a typedef which would look something like:

typedef Emit<State> = void Function(State);

@willlockwood
Copy link

I like this! Mainly for reasons that others have already explained many times over now ☝️

But I have a question—how would the library behave when multiple handlers are defined for the same event type? Would both be called (and in what order), or would one override the other? Would that be prevented somehow (I'm struggling to imagine how that could be done AOT)?

abstract class CounterEvent {}
class Increment extends CounterEvent {}

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

This is the only thing I can come up with that I think represents something less intuitive about this approach than having one mapEventToState method that itself is always is called predictably (even if the internals can definitely get messy for newcomers).

Anyway, I still see this is an improvement and would be excited to work with this API—just curious how you'd plan to approach that!

@felangel
Copy link
Owner Author

@felangel Hi!

Now I start to update to the new flutter_bloc version.

I use freezed package, with the previous version, I can do this:

  @override
  Stream<MyState> mapEventToState(MyEvent event,) =>
      state.map(
          init: (_) =>
              event.maybeMap(
                load: (_) => _load(),
                orElse: () => Stream.empty(),
              ),
          data: (_) =>
              event.maybeMap(
                reload: (_) => _reload(),
                orElse: () => Stream.empty(),
              ),
      );

With the previous version, I can use bloc like FSM without a boilerplate. But now I should check state inside on<Event> handler. Can you help me, how I can migrate better? How I can check state as easily as check event?

You should be able to do:

on<MyEvent>((event, emit) {
  return emit.onEach(
    state.map(
      init: (_) => event.maybeMap(
        load: (_) => _load(),
        orElse: () => Stream.empty(),
      ),
      data: (_) => event.maybeMap(
        reload: (_) => _reload(),
        orElse: () => Stream.empty(),
      ),
    ),
    onData: (state) {
      if (state != null) emit(state);
    },
  );
});

Hope that helps 👍

@matekdk
Copy link

matekdk commented Sep 28, 2021

@felangel : Thanks for a great library! I know this change proposal is already implemented, but could we get a clarification of the following statement from the migration guide:

By default, events will be processed concurrently when using on<E> as opposed to mapEventToState which processes events sequentially.

Example:

on<Event1>( asyncHandler1 ); // emits state(1), awaits some async works, emits state(2)
on<Event2>( syncHandler2 ); // emits state(3) without any 'await's

Does it mean that if bloc receives [ Event1(), Event2() ] the emitted states will be [ state(1), state(3), state(2) ] ?

Is the fact that events are processed concurrently also valid for a single handler ? In previous example, if user feeds the bloc [ Event1(1), Event1(2) ] and handling of Event1(1) takes much longer time then Event1(2), will the output states from the bloc will be state(s) emited from handling of Event1(2) first, and then state(s) emitted from handling of Event1(1) ?

Last question - the migration states "By default, events will be processed concurrently" - so is there a way to actually force a bloc to handle events sequentially ? (which I assume is the case with mapEventToState - at least that is the behaviour we are expecting in two of our apps that are in the app-store and working without issues).

@felangel
Copy link
Owner Author

felangel commented Sep 29, 2021

@matekdk thanks for the support, I really appreciate it!

Does it mean that if bloc receives [ Event1(), Event2() ] the emitted states will be [ state(1), state(3), state(2) ] ?

In that example, the emitted states will be: [state(1), state(3), state(2)] 👍

Is the fact that events are processed concurrently also valid for a single handler ?

Yup by default each event handler processes events concurrently and multiple registered handlers function independently.

In previous example, if user feeds the bloc [ Event1(1), Event1(2) ] and handling of Event1(1) takes much longer time then Event1(2), will the output states from the bloc will be state(s) emitted from handling of Event1(2) first, and then state(s) emitted from handling of Event1(1) ?

Yes, that's correct. If that's undesirable you can apply a transformer like droppable (to ignore the second event if the initial event is still processing) or restartable if you want to cancel the pending event and immediately process the new one 👍

"By default, events will be processed concurrently" - so is there a way to actually force a bloc to handle events sequentially ?

Yes, I updated the migration docs to hopefully make this more explicit but if you want to retain the previous behavior with the new API you can just register one event handler for all events and apply a sequential transformer like:

import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';

class MyBloc extends Bloc<MyEvent, MyState> {
  MyBloc() : super(MyState()) {
    on<MyEvent>(_onEvent, transformer: sequential())
  }

  FutureOr<void> _onEvent(MyEvent event, Emitter<MyState> emit) async {
    // TODO: logic goes here...
  }
}

You can also override the default EventTransformer for all blocs in your application:

import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';

void main() {
  Bloc.transformer = sequential<dynamic>();
  ...
}

Hope that helps!

@Izaird
Copy link

Izaird commented Sep 29, 2021

how can I migrate this?

@override
  Stream<AuthState> mapEventToState( AuthEvent event,) async* {
    yield* event.map(
      authCheckRequested: (e) async*{
        final userOption = await _authFacade.getSignedInUserId();
        yield userOption.fold(
          () => const AuthState.unauthenticated(),
          (_) => const AuthState.authenticated(),
        );

      }, 
      signedOut: (e) async*{
        await _authFacade.signOut();
        yield const AuthState.unauthenticated();
      }
    );
  }
}

@felangel felangel mentioned this issue Oct 2, 2021
7 tasks
@felangel
Copy link
Owner Author

felangel commented Oct 6, 2021

how can I migrate this?

@override
  Stream<AuthState> mapEventToState( AuthEvent event,) async* {
    yield* event.map(
      authCheckRequested: (e) async*{
        final userOption = await _authFacade.getSignedInUserId();
        yield userOption.fold(
          () => const AuthState.unauthenticated(),
          (_) => const AuthState.authenticated(),
        );

      }, 
      signedOut: (e) async*{
        await _authFacade.signOut();
        yield const AuthState.unauthenticated();
      }
    );
  }
}

You could do:

class MyBloc extends Bloc<MyEvent, MyState> {
  MyBloc() : super(MyState.initial()) {
    on<MyEvent>(_onEvent);
  }

  Future<void> _onEvent(MyEvent event, Emit<MyState> emit) async {
    await event.map(
      authCheckRequested: (e) async {
        final userOption = await _authFacade.getSignedInUserId();
        emit(userOption.fold(
          () => const AuthState.unauthenticated(),
          (_) => const AuthState.authenticated(),
        ));
      },
      signedOut: (e) async {
        await _authFacade.signOut();
        emit(const AuthState.unauthenticated());
      },
      ...
  }
}

Hope that helps 👍

@jessicaliu1282
Copy link

jessicaliu1282 commented Oct 7, 2021

Hi, after migrating to v7.2.0 flutter bloc, my bloc didn't handle the event in order.
How can I fix this? Thanks.

@alvinvin00
Copy link

Hi, after migrating to v7.2.0 flutter bloc, my bloc didn't handle the event in order. How can I fix this? Thanks.

Starting with Bloc 7.2.0, all events will be processed concurrently so it will be similar to how Cubit works, Read here for how to retain the old behavior

@jessicaliu1282
Copy link

Hi, after migrating to v7.2.0 flutter bloc, my bloc didn't handle the event in order. How can I fix this? Thanks.

Starting with Bloc 7.2.0, all events will be processed concurrently so it will be similar to how Cubit works, Read here for how to retain the old behavior

Hi, I used the _onEvent function with sequential transformer in my target bloc and it works correctly. Thanks for the help.
Using Bloc.transformer = sequential(); for all blocs didn't help.

But I still have a lot of if ... else if ... in my _onEvent function just like using the mapEventToState function.
So I have to make all blocs using _onEvent function or is there anything else I can do? Thanks.

@alvinvin00
Copy link

alvinvin00 commented Oct 7, 2021

you can do this

class SequentialBloc extends Bloc<SequentialEvent, SequentialState>{

    SequentialBloc(): super(SequentialInitial()) {
        //We use abstract class as generic so all events that implemented it will be handled by _onEvent
        on<SequentialEvent>(_onEvent, transformer : sequential());    
    }

    _onEvent(event, emit){
        //Old logic can be put here
    }
}

This is the pseudocode i wrote without formatting

@jnknewaj
Copy link

@felangel It is very clean, I would like to see a similar way to the Freezed library
state.When/MaybeWhen and state.Map/MaybeMap
and of course state.copyWith too

Thanks! You should still be able to use freezed with when, copyWith, etc... with this approach

class MyBloc extends Bloc<MyEvent, MyState> {
  MyBloc() : super(MyState.initial()) {
    on<MyEvent>(_onEvent);
  }

  void _onEvent(MyEvent event, Emit<MyState> emit) async {
    event.when(
      a: () => emit(state.copyWith(...)),
      b: () => emit(state.copyWith(...)),
      ...
  }
}

void _onEvent(MyEvent event, Emit<MyState> emit) async {...}

Is Emit now renamed to Emitter?

@felangel
Copy link
Owner Author

@felangel It is very clean, I would like to see a similar way to the Freezed library
state.When/MaybeWhen and state.Map/MaybeMap
and of course state.copyWith too

Thanks! You should still be able to use freezed with when, copyWith, etc... with this approach

class MyBloc extends Bloc<MyEvent, MyState> {
  MyBloc() : super(MyState.initial()) {
    on<MyEvent>(_onEvent);
  }

  void _onEvent(MyEvent event, Emit<MyState> emit) async {
    event.when(
      a: () => emit(state.copyWith(...)),
      b: () => emit(state.copyWith(...)),
      ...
  }
}

void _onEvent(MyEvent event, Emit<MyState> emit) async {...}

Is Emit now renamed to Emitter?

Yup 👍
You can check out the migration guide for more info at https://bloclibrary.dev/#/migration

@claudiaaw
Copy link

i'm having some trouble with streams...how would I migrate something that looks like this?

class QuestionWatcherBloc extends Bloc<QuestionWatcherEvent, QuestionWatcherState> {
  final IQuestionRepository _questionRepository;
  QuestionWatcherBloc(
    this._questionRepository,
  ) : super(const QuestionWatcherState.initial());

  @override
  Stream<QuestionWatcherState> mapEventToState(
    QuestionWatcherEvent event,
  ) async* {
    yield const QuestionWatcherState.loadInProgress();
    yield* _questionRepository
        .watchAll()
        .map((failureOrQuestions) => failureOrQuestions.fold(
              (f) => QuestionWatcherState.loadFailure(f),
              (questions) => QuestionWatcherState.loadSuccess(questions),
            ));
  }
}

where the watchAll() is of type Stream<Either<QuestionFailure, KtList<Question>>> and listens to any changes in firebase?

@matekdk
Copy link

matekdk commented Nov 7, 2021

A workaround for this change is simply to map the old mapEventToState function in the on handler, like this:

on<EventType>((event, emit) async {
      await emit.onEach(mapEventToState(event), onData: (StateType state) {
        emit(state);
      });
}, transformer: sequential());

And then just remove @override annotation from mapEventToState. Note, that sequential() transformer comes from bloc_concurrency package.

@binaryartifex
Copy link

@felangel firstly, absolutely well done on this package. just a quick one, ive been very hard pressed to find a reliable solution to this particular problem domain - connectivity/network connections. My goal here is a simple subscription to give me a yay or nay if my network is not only active, but there is a valid connection available. The event is a singular event returning a boolean value. State is one of Connected or Disconnected. Third party libraries are injected via getIt and Injectable. Id be greatly appreciative of anyones two cents on this snippet. Cheers folks.

@injectable
class ConnectivityBloc extends Bloc<ConnectivityEvent, ConnectivityState> {
  final Connectivity connectivity;
  final InternetConnectionChecker connectionChecker;
  StreamSubscription<ConnectivityResult>? _streamSubscription;

  ConnectivityBloc({
    required this.connectivity,
    required this.connectionChecker,
  }) : super(const Disconnected()) {
    on<ConnectionChanged>(_onConnectionChanged);

    _streamSubscription = connectivity.onConnectivityChanged.listen((status) async {
      if (status != ConnectivityResult.none) {
        final hasConnection = await connectionChecker.hasConnection;
        add(ConnectionChanged(hasConnection: hasConnection));
      }
    });
  }

  @override
  Future<void> close() {
    _streamSubscription?.cancel();
    return super.close();
  }

  void _onConnectionChanged(ConnectionChanged event, Emitter<ConnectivityState> emit) {
    final state = event.hasConnection ? const Connected() : const Disconnected();
    emit(state);
  }
}

@felangel
Copy link
Owner Author

@felangel firstly, absolutely well done on this package. just a quick one, ive been very hard pressed to find a reliable solution to this particular problem domain - connectivity/network connections. My goal here is a simple subscription to give me a yay or nay if my network is not only active, but there is a valid connection available. The event is a singular event returning a boolean value. State is one of Connected or Disconnected. Third party libraries are injected via getIt and Injectable. Id be greatly appreciative of anyones two cents on this snippet. Cheers folks.

@injectable
class ConnectivityBloc extends Bloc<ConnectivityEvent, ConnectivityState> {
  final Connectivity connectivity;
  final InternetConnectionChecker connectionChecker;
  StreamSubscription<ConnectivityResult>? _streamSubscription;

  ConnectivityBloc({
    required this.connectivity,
    required this.connectionChecker,
  }) : super(const Disconnected()) {
    on<ConnectionChanged>(_onConnectionChanged);

    _streamSubscription = connectivity.onConnectivityChanged.listen((status) async {
      if (status != ConnectivityResult.none) {
        final hasConnection = await connectionChecker.hasConnection;
        add(ConnectionChanged(hasConnection: hasConnection));
      }
    });
  }

  @override
  Future<void> close() {
    _streamSubscription?.cancel();
    return super.close();
  }

  void _onConnectionChanged(ConnectionChanged event, Emitter<ConnectivityState> emit) {
    final state = event.hasConnection ? const Connected() : const Disconnected();
    emit(state);
  }
}

Hi 👋
Thanks for the positive feedback, I really appreciate it! 🙏 💙

Regarding your question, you can do something like:

enum ConnectivityState { online, offline, unknown }

class ConnectivityBloc extends Bloc<ConnectivityEvent, ConnectivityState> {
  ConnectivityBloc({
    required ConnectivityRepository connectivityRepository,
  })  : _connectivityRepository = connectivityRepository,
        super(ConnectivityState.unknown) {
    on<_ConnectivityChanged>((event, emit) => emit(event.connectivityState));
    on<ConnectivityRequested>(_onConnectivityRequested);
    _connectivityChangedSubscription = _connectivityRepository
        .onConnectivityChanged
        .listen(_connectivityChanged);
  }

  final ConnectivityRepository _connectivityRepository;
  late StreamSubscription<ConnectivityStatus> _connectivityChangedSubscription;

  void _connectivityChanged(ConnectivityStatus connectivityStatus) {
    add(
      _ConnectivityChanged(
        connectivityState: connectivityStatus.toConnectivityState(),
      ),
    );
  }

  Future<void> _onConnectivityRequested(
    ConnectivityRequested event,
    Emitter<ConnectivityState> emit,
  ) async {
    try {
      final status = await _connectivityRepository.connectivityStatus();
      emit(status.toConnectivityState());
    } catch (_) {
      emit(ConnectivityState.unknown);
    }
  }

  @override
  Future<void> close() {
    _connectivityChangedSubscription.cancel();
    return super.close();
  }
}

extension on ConnectivityStatus {
  ConnectivityState toConnectivityState() {
    switch (this) {
      case ConnectivityStatus.offline:
        return ConnectivityState.offline;
      case ConnectivityStatus.online:
        return ConnectivityState.online;
    }
  }
}

Hope that helps 👍

@KDCinfo
Copy link

KDCinfo commented Nov 30, 2021

I hope external links are alright: @binaryartifex Flutterly has provided an in-depth walkthrough of using the connectivity_plus package using blocs as part of his full-length Flutter Bloc presentation tutorial.

The chapter starts at [1:50:13] and the connectivity part starts at [1:51:40].

https://pub.dev/packages/connectivity_plus

I'm looking forward to seeing what I can integrate from Felix's code above with Flutterly's approach.

@Faaatman
Copy link

Faaatman commented Jan 3, 2022

Hello, @felangel hope you're doing great! I absolutely love the update and how easy and seamless it is to migrate my blocs!
I just have a quick question.
How would I wrap all of my events with a single try and catch since whatever I'm doing on catch is the same for both events?

  MyBloc() : super(MyBlocInitial()) {
    on<MyBlocLoad>(_onMyBlocLoad);
    on<MyBlocRedeem>(_onMyBlocRedeem);
  }

Happy new year and thank you so much for this awesome package!

@plibr
Copy link

plibr commented Feb 20, 2022

Hi, @felangel! I am newbie in Flutter. I use flutter_bloc: 8.0.1

How can I convert to on(..) style this:
@override
Stream<RemoteArticlesState> mapEventToState(RemoteArticlesEvent event) async* {
if (event is GetArticles) yield* _getBreakingNewsArticle(event);
}

It is much more complex use case. Here we have to use generator for stream.

The project is here:

https://github.com/devmuaz/flutter_clean_architecture/blob/part2_remote_data/lib/src/presentation/blocs/remote_articles/remote_articles_bloc.dart

@Gene-Dana
Copy link
Collaborator

Gene-Dana commented Feb 23, 2022

How can I convert to on(..) style this:

Have you seen this? #2526 (comment)

Also, I believe this is the recommended approach for working with streams ! emit.forEach

Future<void> _onSubscriptionRequested(
TodosOverviewSubscriptionRequested event,
Emitter<TodosOverviewState> emit,
) async {
emit(state.copyWith(status: () => TodosOverviewStatus.loading));
await emit.forEach<List<Todo>>(
_todosRepository.getTodos(),
onData: (todos) => state.copyWith(
status: () => TodosOverviewStatus.success,
todos: () => todos,
),
onError: (_, __) => state.copyWith(
status: () => TodosOverviewStatus.failure,
),
);
}

@jicasa03
Copy link

Hi, @felangel! I am newbie in Flutter. I use flutter_bloc: 8.0.1

How can I convert to on(..) style this:

class FragmentBloc extends Bloc<FragmentEvent, FragmentState> {
@OverRide
FragmentState get initialState => FragmentInitialState();
@OverRide
Stream mapEventToState(
FragmentEvent event,
) async* {
if (event is FragmentNavigateEvent) {
if (!FragmentManager().isExist(event.routeName)) {
FragmentManager().addFragment(event.routeName);
}
final int currentIndex =
FragmentManager().navigateToName(event.routeName);
// print('bloc currentIndex:$currentIndex');
yield FragmentCurrentState(currentIndex);
} else if (event is FragmentBackEvent) {
final int currentIndex = FragmentManager().navigateBack();
if (currentIndex < 0) {
yield FragmentNoBackState();
} else {
yield FragmentBackState(currentIndex);
}
}
}
}

@mateusfccp
Copy link

mateusfccp commented Apr 5, 2022

If static metaprogramming was being made in a way that allow us to specify DSL we improve it even further, like:

bloc CounterBloc on int with CounterEvent {
  default is 0;
  
  on Increment => emit(state + 1);
  on Decrement => emit(state - 1);
}

However, it's going to a completely different approach, sadly...

@grfn13
Copy link

grfn13 commented Nov 7, 2022

How would I migrate this to the new version?

event

@anandi111
Copy link

this is completely awesome and i really like it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion Open discussion for a specific topic enhancement candidate Candidate for enhancement but additional research is needed feedback wanted Looking for feedback from the community pkg:bloc This issue is related to the bloc package
Projects
None yet
Development

Successfully merging a pull request may close this issue.