Search code examples
flutterfirebaseauthenticationblocstate-management

BlocListener doesn't listen a changing state


I try to solve that problem a couple of days, but I can't. I have a login form, when user signs in, the LoginBloc changes its state to LogIn() and BlocListener(which is located in HomeView.dart) must change HomeBloc's state from Initial() to Loaded(), but it doesn't work. I don't know why but BlocListener ignores that LoginBloc has LogIn() state and doesn't run HomeBlocEvent.loadUserDetails() I appreciate any help or pieces of advice how to solve this problem. I used Firebase as Authentication service.

// LoginBloc.dart
@injectable
class LoginBloc extends Bloc<LoginEvent, LoginState> {
  final UserSignInImpl _userSignInImpl;

  LoginBloc(this._userSignInImpl) : super(const LoginState.initial()) {
    on<SignIn>((event, emit) async {
      emit(const LoginState.loading());
      final result = await _userSignInImpl.call(event.user);
      result.fold((error) {
        emit(LoginState.loadFailed(error));
      }, (success) {
        emit(LoginState.logIn(user: success));
      });
    });
  }
}
  // HomeView.dart
  @override
  Widget build(BuildContext context) {
    final homeBloc = BlocProvider.of<HomeBloc>(context);
    final loginBloc = BlocProvider.of<LoginBloc>(context);
      ......
      body: BlocListener<LoginBloc, LoginState>(
        listener: (context, state) {
          state.maybeWhen(
            // function below isn't called, but LoginState is LogIn()
            logIn: (user) => homeBloc.add(
              HomeEvent.loadUserDetails(user.email!),
            ),
            orElse: () {},
          );
        },
        child: NestedScrollView(
          headerSliverBuilder: (context, bool innerBoxIsScrolled) => [
            const HomeSliverAppBar(),
          ],
          body: BlocBuilder<HomeBloc, HomeState>(
            bloc: homeBloc,
            builder: (context, state) {
              return state.maybeWhen(
                loaded: (userInfo) => const HomeListView(),
                // TODO: implement shimmer view
                orElse: () => const Center(child: CircularProgressIndicator()),
              );
            },
          ),
        ),
      ),
// HomeBloc.dart
@injectable
class HomeBloc extends Bloc<HomeEvent, HomeState> {
  HomeBloc() : super(const HomeState.initial()) {
    on<LoadUserDetails>((event, emit) async {
      emit(const HomeState.loading());
      await fbFireStore.getUserDetails(event.email).then(
            (userDetails) => emit(HomeState.loaded(user: userDetails)),
          );
    });
    on<SignOut>((event, emit) async {
      await fbAuth.signOut();
    });
  }
}

  // LoginView.dart
  @override
  Widget build(BuildContext context) {
    final loginBloc = BlocProvider.of<LoginBloc>(context);
  .....
                        child: ElevatedButton(
                        style: AppStyles.widePurpleButtonStyle,
                        onPressed: () {
                          if (_signInKey.currentState!.validate()) {
                           // Adding SignIn() event                            
                           loginBloc.add(LoginEvent.signIn(LoginUser(
                              email: _emailController.text.trim(),
                              password: _passwordController.text.trim(),
                            )));
                          }
                        },
                        child: Text(S.of(context).logIn),
                      ),
    .....
                   SizedBox(
                    height: MediaQuery.sizeOf(context).height * 0.05,
                    child: BlocConsumer<LoginBloc, LoginState>(
                      builder: (context, state) {
                        return state.maybeWhen(
                          loading: () {
                            return const CircularProgressIndicator();
                          },
                          orElse: () {
                            return const SizedBox();
                          },
                        );
                      },
                      listener: (context, state) {
                        state.maybeWhen(
                          loadFailed: (failure) {
                            showDialog(
                              context: context,
                              builder: (context) {
                                return ErrorAlertDialog(error: failure);
                              },
                            );
                          },
                          logIn: (_) {
                            context.router.navigate(const HomeRoute());
                          },
                          orElse: () {},
                        );
                      },
                    ),
                  ),

BlocListener must run HomeEvent.loadUserDetails() when LoginBloc's state is LogIn()


Solution

  • The thing is that when LoginBloc emits LoginState.logIn() your HomeView is not yet in the Navigation stack. Note that you are performing context.router.navigate(const HomeRoute()); inside listener of BlocConsumer<LoginBloc, LoginState>() in LoginView.

    So when HomeView is actually opened LoginBloc state is LoginState.logIn() already and no other states are emitted by LoginBloc and that's why your listener in HomeView is not receiving anything.

    To access user details inside HomeView you can just get LoginBloc state:

    final state = loginBloc.state; // LoginState.login(user: ...)
    

    But I suggest just to pass a user email as a parameter for HomeView and then use it to load user details.