Search code examples
flutterbloc

flutter bloc: how to wait for an event the return some state data


I am trying to integrate flutter_login widget with bloc. Here is the sample code I am using

BlocProvider(
      create: (ctx) => UserAuthenticationPageBloc(ctx),
      child: BlocListener<UserAuthenticationPageBloc, UserAuthenticationPageState>(
        listener: (context, state) {
          // not used at the moment
        },
        child: BlocBuilder<UserAuthenticationPageBloc, UserAuthenticationPageState>(
          builder: (context, state) {
            final bloc = context.read<UserAuthenticationPageBloc>();

            return FlutterLogin(onLogin: (loginData) async {
              bloc.add(SignIn(loginData: loginData));
              return state.loginMessage;
            }, onRecoverPassword: (email) async {
              bloc.add(RecoverPassword(email: email));
              return state.recoverPasswordMessage;
            });
          },
        ),
      ),
    )

Here is the bloc file

class UserAuthenticationPageBloc extends Bloc<UserAuthenticationPageEvent, UserAuthenticationPageState> {
  UserAuthenticationPageBloc(BuildContext context) : super(const UserAuthenticationPageState()) {
    on<SignIn>((event, emit) {
      try {
        emit(state.copyWith(signInStatus: SignInStatus.loading));

        User user = User(); // need to be replaced by async http call

        final GlobalBloc globalBloc = BlocProvider.of<GlobalBloc>(context);

        globalBloc.add(GlobalSignIn(user: user));
        emit(state.copyWith(loginMessage: 'some error', signInStatus: SignInStatus.failure));
      } catch (_) {
        //emit(CovidError("Failed to fetch data. is your device online?"));
      }
    });
    on<RecoverPassword>((event, emit) {
    });
  }
}

What I would like to do is to add an event to bloc and then return a message. The flutter_login widget will show snack bar based on the message returned.

How can I wait for bloc event to finish before retrieving the loginMessage from the state? Or maybe I should not put the loginMessage in state?

Thank you


Solution

  • In most scenarios, these types of things can all be handled pretty easily with the BlocListener widget. Usually, in the UI you don't have to manually await anything coming from a bloc stream. This is one exception, where you're trying to utilize the built in UI elements of flutter_widget.

    Since the onLogin callback requires a return of null on success or error message string on failure, you want to await the stream directly.

    The syntax for that looks like this

     final authBloc = context.read<GlobalBloc>();
    // fire the sign in event
    
    // You know the first emitted state will be loading, so the next state is either success or failure
    
     await authBloc.stream.firstWhere(
                  (state) => state.status != AuthStatus.loading,
                );
    
    

    The whole widget would look something like this.

    FlutterLogin(
              onLogin: (loginData) async {
                final authBloc = context.read<GlobalBloc>()
                  ..add(
                    SignIn(
                      email: loginData.name,
                      password: loginData.password,
                    ),
                  );
    
                await authBloc.stream.firstWhere(
                  (state) => state.status != AuthStatus.loading,
                );
    
                return authBloc.state.status == AuthStatus.authenticated
                    ? null
                    : 'Login failed';
              },
              onRecoverPassword: (_) async {
                return null;
              },
            );
    

    A couple things with what you shared.

    1. BlocBuilder widgets should not be placed directly below the BlocProvider. So in this case, to avoid that you could just create a new widget with the BlocBuilder wrapping a FlutterLogin. Although as you can see, in this case to use the built in snackbar, you don't need the BlocBuilder here.
    2. You're calling an event from the build method that makes network requests. This is pretty much always a bad idea, because we don't always have full control over how many times the build method is called and you run the risk of making way to many unintended network calls.
    3. Your UserAuthenticationPageBloc depends directly on GlobalBloc, which according to the docs should be avoided at all times. This is easily avoided with the BlocListener widget. I'm also not seeing why there are 2 separate blocs here at all just for login. So my example was simplified to just one Bloc class that handles login.

    The gif below shows a 2 second wait and the bloc emitting an error state.

    Edit

    To answer your question in the comment:

    I get that you want the auth info to be globally accessible, that's pretty common. Typically I would have my AuthBloc at the top of the widget tree along with any other Bloc that needs to be globally accessible. Not sure sure if you have other stuff going on in your GlobalBloc besides login, if not, just I'd just rename that to something like AuthBloc and get rid the separate one just for the login page.

    If you do have other stuff going on in your GlobalBloc, I'd still just create an AuthBloc at the top of the widget tree and use that to manage the state of your login page, and that bloc would only be responsible for authentication. Either way, accessing blocs directly from other blocs makes testing much harder and is easily avoidable by utilizing the BlocListener widget. That is at least true when initially setting up blocs and general architecture. If you ignore all of that and when things get complex and you have lots of direct dependencies going on, it gets much harder to separate.

    I would also avoid passing in BuildContext to a Bloc class. Even if you do need another Bloc, I would pass in the Bloc object directly (but again, don't do that). A bloc depending on BuildContext will also make testing harder.

    About network calls in the build method, after re-reading your code this is my mistake. I see that you're just passing in callback to the onLogin param of the FlutterLogin widget, not actually making a network request in the build method, sorry for the confusion there.

    Feel free to share your GlobalBloc if you're still looking for refactor help.