felangel / bloc

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

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

Closed felangel closed 3 years ago

felangel commented 3 years ago

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

nazrinharris commented 3 years ago

Hey, I've been reading about this and am very excited for the changes and the simplicity that comes with it. I was brought here while having problems with nested async generators and I just couldn't find the solution for it so I wanted to try out the new changes.

But I would like some help regarding this. I copy pasted the overrides and ran pub get and everything went smoothly. I updated one of my blocs to use the new on<> and it was all fine until something went wrong (I'm not sure what) and I had to run pub get again. This was the error I received

Git error. Command: `git fetch`
stdout: 
stderr: error: cannot lock ref 'refs/heads/feat/replace-mapEventToState-with-on<E>': Unable to create 'C:/Users/unoff/AppData/Roaming/Pub/Cache/git/cache/bloc-f84666e53401b96b922e28d5fd764038224e0c04/./refs/heads/feat/replace-mapEventToState-with-on<E>.lock': Invalid argument
From https://github.com/felangel/bloc
 ! 7a679116..aad16060  feat/replace-mapEventToState-with-on<E> -> feat/replace-mapEventToState-with-on<E>  (unable to update local ref)
exit code: 1
pub get failed (server unavailable) -- attempting retry 2 in 2 seconds...

I've tried running flutter clean, flutter pub cache repair and it still wouldn't work.

Any help is appreciated!

Also, when I was updating my bloc, none of my other blocs have errors, while they were using mapEventToState, was that supposed to happen? :- Though the the blocs didnt have syntax errors, the actual bloc does not work on runtime.

Update : I managed to fix this by reinstalling flutter as a whole. The problem probably rose from me not removing fvm correctly and my flutter directory was more or less a mess. Cleaning this up fixed it :)

Here's me using the bloc with no async generators and working perfectly fine. I love it so much I could marry it. Before, when I wanted to run some methods on my CabinetCubit I would have to use async* to yield states, and in my case it just wouldn't work, now it does. Thanks felix. (✿◠‿◠).

list_screen_bloc.dart

class ListScreenBloc extends Bloc<ListScreenEvent, ListScreenState> {
  final CabinetCubit cabinetCubit;
  final AuthCubit authCubit;

  ListScreenBloc({
    required this.authCubit,
    required this.cabinetCubit,
  }) : super(const LSInitial()) {
    on<_Started>(start);
    on<LSexecGetAllListSGSnip>(execGetAllListSGSnip);
  }

  FutureOr<void> start(
    ListScreenEvent event,
    Emitter<ListScreenState> emit,
  ) {
    emit(state);
  }

  FutureOr<void> execGetAllListSGSnip(
    ListScreenEvent event,
    Emitter<ListScreenState> emit,
  ) {
    print('execGetAllListSGSnip started');

    emit(const ListScreenState.loading(
        currentOperationMessage: 'Retrieving your groups..'));

    String uid;

    authCubit.execGetUserUid().then((_) async {
      print('execGetUserUid completed');

      AuthState authState = authCubit.state;

      if (authState is AuthGeneralCompleted) {
        print('UID Retrieved');
        uid = authState.content;

        await cabinetCubit.execGetAllList(uid: uid).then(
          (_) async {
            CabinetState cabinetState = cabinetCubit.state;

            if (cabinetState is CabinetLoadedGetAllListSGSnip) {
              emit(ListScreenState.loadedAllListSGSnip(
                  cabinetState.allListSGSnip));
            } else if (cabinetState is CabinetErrorGetAllListSGSnip) {
              emit(ListScreenState.error(
                message: cabinetState.message,
                code: cabinetState.code,
              ));
            } else {
              print('cabinetCubit unexpected state ; $cabinetState');
              throw UnexpectedException(
                code: 'cabinet_cubit_exception',
                message: 'what the hej happened',
              );
            }

            print('execGetAllList completed');
          },
        );
      } else if (authState is AuthError) {
        print('AuthCubit Error');
        emit(
          ListScreenState.error(
              message: authState.message,
              code: authState.code ?? 'No code was provided'),
        );
      } else {
        print('authCubit unexpected state ; $authState');
        throw UnexpectedException(code: 'code', message: 'authCubit');
      }
    });
  }
}
datdefboi commented 3 years ago

was that supposed to happen?

yeah, that's it. Besides, when you send any event that has no listener (on), you will see an error.

felangel commented 3 years ago

@datdefboi just updated replay_bloc in 1baa6820629187bbb23a7d799b8c7e9fe73f9a85 let me know if you run into any issues and thanks for your patience!

orestesgaolin commented 3 years ago

I think it would be awesome to have an intermediate release that would let all the people know that the old API is going to be deprecated. I think only a limited number of people are aware of the proposal and having a deprecation notice in the code might be a really good way to gather more feedback.

felangel commented 3 years ago

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! 💙 🙏

felangel commented 3 years ago

🥁 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! 🙏 💙

felangel commented 3 years ago

Update

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

felangel commented 3 years ago

Update

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

zbarbuto commented 3 years ago

Following this comment I'm wondering if it would be worth encouraging a different name convention for the emit argument used in on() to avoid confusion with the internal emit used by Bloc / BlocBase.

If you're migrating from mapEventToState it seems like an easy mistake to make that you forget to pass in the emit provided by on and end up using the internal emit. For example, let's imagine we migrated this bloc from a combination of bloc and freezed (using map):

// Renamed from `mapEventToState`
class MyBloc {
  MyBloc() {
    on<MyEvent>(myEventHandler);
  }

  // renamed/migrated from mapEventToState
  myEventHandler(event, emit) {
    event.map(
     eventA: (event, emit) => eventAHandler(event) // whoops, we didn't pass in `emit`
    )
  }

  eventAHandler(event) async {
    emit(newState); // this "works" but it's using the internal bloc emit
  }
}

It's a bit of an edge-case but it does seem open to confusion which could be avoided by just encouraging people to use emitState,setState or some other name.

Another option might be to remove @protected from the internal emit and just leave it as @visibleForTesting. It seems that because it annotated with both @protected and @visibleForTesting that the editor does not complain about using it (in VSCode at least):

For example, this is using the internal emit:

image

However, if you remove @protected and leave the @visibleForTesting you get the warning as expected:

The same line after manually removing @protected from it:

image

felangel commented 3 years ago

Following this comment I'm wondering if it would be worth encouraging a different name convention for the emit argument used in on() to avoid confusion with the internal emit used by Bloc / BlocBase.

If you're migrating from mapEventToState it seems like an easy mistake to make that you forget to pass in the emit provided by on and end up using the internal emit. For example, let's imagine we migrated this bloc from a combination of bloc and freezed (using map):

// Renamed from `mapEventToState`
class MyBloc {
  MyBloc() {
    on<MyEvent>(myEventHandler);
  }

  // renamed/migrated from mapEventToState
  myEventHandler(event, emit) {
    event.map(
     eventA: (event, emit) => eventAHandler(event) // whoops, we didn't pass in `emit`
    )
  }

  eventAHandler(event) async {
    emit(newState); // this "works" but it's using the internal bloc emit
  }
}

It's a bit of an edge-case but it does seem open to confusion which could be avoided by just encouraging people to use emitState,setState or some other name.

Another option might be to remove @protected from the internal emit and just leave it as @visibleForTesting. It seems that because it annotated with both @protected and @visibleForTesting that the editor does not complain about using it (in VSCode at least):

For example, this is using the internal emit:

image

However, if you remove @protected and leave the @visibleForTesting you get the warning as expected:

The same line after manually removing @protected from it:

image

Good catch! I removed the protected decorator 👍

felangel commented 3 years ago

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

karabanovbs commented 3 years ago

@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?

felangel commented 3 years ago

@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 commented 3 years ago

@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 commented 3 years ago

@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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

@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 commented 3 years ago

@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 commented 3 years ago

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 commented 3 years ago

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 commented 2 years ago

@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 commented 2 years ago

@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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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

Have you seen this? https://github.com/felangel/bloc/issues/2526#issuecomment-1046214602

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

https://github.com/felangel/bloc/blob/b6c55144e6c37c57100cf5be1125dca8bcc05b48/examples/flutter_todos/lib/todos_overview/bloc/todos_overview_bloc.dart#L26-L42

jicasa03 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

How would I migrate this to the new version?

event

anandi111 commented 1 year ago

this is completely awesome and i really like it