flutter_login and flutter_bloc navigation after authentication: BlocListener not listening to state change

Issue

I am trying to combine this with bloc, using this design pattern from the docs.

After the state has been instantiated, BlocListener stops listening to the authentication bloc and I am kind of forced to use the login form’s onSubmitAnimationCompleted method for routing, which makes the listener useless in the first place.

  • MaterialApp() is identical to the example provided in the docs (I am trying to navigate from the login screen, which is the initialRoute in this case, to the home screen)

  • the login form looks like this:

@override
  Widget build(BuildContext context) {
    return BlocListener<AuthenticationBloc, AuthenticationState> (
      listener: (context, state) {
        // first time around state is read
        if (state is AuthenticationAuthenticated) {
          Navigator.of(context).pushNamed(Home.routeName);
        }
      },
      child: BlocBuilder(
        bloc: _loginBloc,
        builder: (BuildContext context, state) {
          return FlutterLogin(
            title: 'Login',
            logo: const AssetImage('lib/assets/madrid.png'),
            onLogin: _authUser,
            onSignup: _signupUser,
            onRecoverPassword: _recoverPassword,
            loginProviders: <LoginProvider>[
              ... Providers here...
            ],
            // if this method is omitted, I'll get a [ERROR:flutter/lib/ui/ui_dart_state.cc(209)]
            onSubmitAnimationCompleted: () {
              Navigator.of(context).pushNamed(Home.routeName);
            },
          );
        },
      ),
    );
  }
  • I am splitting events an state between two blocs, ‘AuthenticationBloc’ (wraps entire app, if a token has been stored then the state will be ‘AuthenticationAuthenticated’) and ‘LoginBloc’ (used for login/logout events)

#1 when I click on the sign up button, the associated method will call _loginBloc?.add(SignUpButtonPressed(email: email, password: password))

#2 fast forward to the bloc:

LoginBloc({required this.authenticationBloc, required this.loginRepository})
    : super(const SignInInitial()) {
      on<SignUpButtonPressed>(_signUp);
    }

...

FutureOr<void> _signUp<LoginEvent>(SignUpButtonPressed event, Emitter<LoginState> emit) async {
    emit(const SignInLoading());

    try {

      final credentials = User(email: event.email, password: event.password);
      final success = await loginRepository.signUp(credentials);

      if (success) {
        final token = await loginRepository.signIn(credentials);
        authenticationBloc.add(LoggedIn(email: event.email, token: token));
      } else {
        emit(const SignInFailure(error: 'Something went wrong'));
      }

    } on Exception {
      emit(const SignInFailure(error: 'A network Exception was thrown'));
    } catch (error) {
      emit(SignInFailure(error: error.toString()));
    }
  }

  • this is successful, and it triggers the authentication bloc:
  AuthenticationBloc({required this.userRepository})
    : super(const AuthenticationUninitialized()) {
      on<LoggedIn>(_loggedIn);
    }

...

  FutureOr<void> _loggedIn<AuthenticationEvent>(LoggedIn event, Emitter<AuthenticationState> emit) async {
    await userRepository?.persistEmailAndToken(
        event.email, event.token);
    await _initStartup(emit);
  }

...

  Future<void> _initStartup(Emitter<AuthenticationState> emit) async {
    final hasToken = await userRepository?.hasToken();

    if (hasToken != null && hasToken == true) {
      emit(const AuthenticationAuthenticated());
      return;
    } else {
      emit(const AuthenticationUnauthenticated());
    }
  }

… and at the end of this, the state is updated to AuthenticationAuthenticated, which is the expected behaviour, and the observer logs the transition as expected.

Now, this state change should trigger the navigation from within the BlocListener, but nope.

I would like to get rid of the Navigator inside the onSubmitAnimationCompleted, and rely on the state change.

I reckon this might be caused by Equatable, as my state extends that:

abstract class AuthenticationState extends Equatable {
  const AuthenticationState();

  @override
  List<Object> get props => [];
}

class AuthenticationAuthenticated extends AuthenticationState {
  const AuthenticationAuthenticated();
}

However, I’ve tried for hours, but I can’t find anything in the docs, github, or SO that works.

Solution

So, I have not been able to get rid of the Navigator inside of onSubmitAnimationCompleted (I guess the BlocListener is disposed when the form is submitted, and before the animation is completed), but in the process I’ve managed to make my state management clean and robust, so I’ll leave a little cheatsheet below, feel free to comment or give your opinion:

  • Assuming your widget’s build method looks something like this:
  @override
  Widget build(BuildContext context) {
    return BlocListener<AuthenticationBloc, AuthenticationState> (
      bloc: _authenticationBloc,
      listener: (context, state) {
        if (state.status == AuthenticationAppState.authenticated) {
          Navigator.of(context).pushNamed(Home.routeName);
        }
      },
      child: BlocBuilder(
        bloc: _loginBloc,
        builder: (BuildContext context, state) {
          return FlutterLogin(
      ...
  • and that your events extend Equatable
import 'package:equatable/equatable.dart';

abstract class AuthenticationEvent extends Equatable {
  const AuthenticationEvent();

  @override
  List<Object> get props => [];
}

class LoggedIn extends AuthenticationEvent {
  final String email;
  final dynamic token;
  const LoggedIn({ required this.email, this.token });

  @override
  List<Object> get props => [email, token];
}
  • your Bloc will look like:
class AuthenticationBloc extends Bloc<AuthenticationEvent, AuthenticationState> {
  final SecureStorage? userRepository;

  AuthenticationBloc({required this.userRepository})
    : super(const AuthenticationState.uninitialized()) {
      on<LoggedIn>(_loggedIn);
      on<LoggedOut>(_loggedOut);
      on<UserDeleted>(_userDeleted);
    }

  ...
  FutureOr<void> _loggedOut<AuthenticationEvent>(LoggedOut event, Emitter<AuthenticationState> emit) async {
    emit(const AuthenticationState.loggingOut());
    await userRepository?.deleteToken();
    // API calls here
    // event has access the event's properties e.g. event.email etc
  }
  • the state has been refactored to:
import 'package:equatable/equatable.dart';

enum AuthenticationAppState {
  uninitialized,
  unauthenticated,
  authenticated,
  loggingOut,
  loading,
}

class AuthenticationState extends Equatable {
  const AuthenticationState._({
    required this.status,
});

  const AuthenticationState.uninitialized() : this._(status: AuthenticationAppState.uninitialized);
  const AuthenticationState.unauthenticated() : this._(status: AuthenticationAppState.unauthenticated);
  const AuthenticationState.authenticated() : this._(status: AuthenticationAppState.authenticated);
  const AuthenticationState.loggingOut() : this._(status: AuthenticationAppState.loggingOut);
  const AuthenticationState.loading() : this._(status: AuthenticationAppState.loading);

  final AuthenticationAppState status;

  @override
  List<Object> get props => [status];
}

Answered By – Fi Li Ppo

Answer Checked By – Jay B. (FlutterFixes Admin)

Leave a Reply

Your email address will not be published. Required fields are marked *