8

I'm using flutter Bloc to navigate the user toward either the login page or home screen depending on whether they are authenticated or not. However, after the initial state change, the listener doesn't trigger when I change my authentication state.

Listener:

Widget build(BuildContext context) {
  return BlocListener<AuthenticationBloc, AuthenticationState>(
    listener: (context, state) {
      // Listener never gets called on statechange after the initial app startup

      // navigation based on authentication 
      }
    },
    child: SplashPage(),
  );
}

The provider gets initialized in the parent widget:

AuthenticationRepository authRepo = AuthenticationRepository();

Widget build(BuildContext context) {
  return MultiBlocProvider(
    providers: [
      BlocProvider<AuthenticationBloc>(
        create: (BuildContext context) =>
            AuthenticationBloc(authenticationRepository: authRepo),
      ),
      /*
         Other Providers
      */
    ],
    child: MaterialApp(
      title: 'myApp',
      home: StartUpPage(),
    ),
  );

When the user logs in mapEventState gets called in the AuthenticationBloc:

class AuthenticationBloc
    extends Bloc<AuthenticationEvent, AuthenticationState> {
  AuthenticationBloc({
    @required AuthenticationRepository authenticationRepository,
  })  : assert(authenticationRepository != null),
        _authenticationRepository = authenticationRepository,
        super(const AuthenticationState.unknown()) {
    _userSubscription = _authenticationRepository.userStream.listen(
      (user) => add(AuthenticationUserChanged(user)),
    );
  }

  final AuthenticationRepository _authenticationRepository;
  StreamSubscription<User> _userSubscription;

  @override
  Stream<AuthenticationState> mapEventToState(
    AuthenticationEvent event,
  ) async* {
    if (event is AuthenticationUserChanged) {
      yield _mapAuthenticationUserChangedToState(event);
    } else if (event is AuthenticationLogoutRequested) {
      unawaited(_authenticationRepository.logOut());
    }
  }

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

  AuthenticationState _mapAuthenticationUserChangedToState(
    AuthenticationUserChanged event,
  ) =>
      event.user != User.empty
          ? AuthenticationState.authenticated(event.user)
          : const AuthenticationState.unauthenticated();
}

I'd expect the listener to trigger when the user logs in and the AuthenticationState changes. If anyone knows what I'm doing wrong or if I'm missing something I'd love to hear it.

EDITED 08-02-2021:

I've checked again if the state changes after login in using a simple button. With this, I can confirm that the state does change and holds the correct user data and authentication status. Another thing I confirmed is that a BlocBuilder that is using a BlocBuilder<AuthenticationBloc, AuthenticationState> IS updating correctly when a user logs in.

EDITED 10-02-2021:

Entire Authentication state:

enum AuthenticationStatus { authenticated, unauthenticated, unknown }

class AuthenticationState extends Equatable {
  const AuthenticationState._({
    this.status = AuthenticationStatus.unknown,
    this.user = User.empty,
  });

  const AuthenticationState.unknown() : this._();

  const AuthenticationState.authenticated(User user)
      : this._(status: AuthenticationStatus.authenticated, user: user);

  const AuthenticationState.unauthenticated()
      : this._(status: AuthenticationStatus.unauthenticated, user: User.empty);

  final AuthenticationStatus status;
  final User user;

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

EDITED 12-02-2021:

removed non-relevant code

EDITED 15-02-2021:

Added entire Authentication BloC

Hamed
  • 5,867
  • 4
  • 32
  • 56
Niek
  • 187
  • 1
  • 8
  • What _does_ happen when you attempt change the state? Are you sure the state is actually changing? – Abion47 Feb 05 '21 at 19:23
  • @Abion47 When a User gets pushed on the User stream following a sign in, an AuthenticationUserChanged event gets added to the authentication Bloc > mapEventToState > _mapAuthenticationUserChangedToState(event) > AuthenticationState.(un)authenticated gets called > a new Authentication state gets created and then yielded from mapEventToState. I've checked that these states are different. – Niek Feb 08 '21 at 08:04
  • Can you provide the `AuthenticationState` code? – Nabeel Parkar Feb 09 '21 at 12:23
  • @NabeelParkar Code added. On startup the state changes from unknown to unauthenticated with an empty user. This gets caught by the listener, subsequent changes do not. – Niek Feb 10 '21 at 07:31
  • Can you provider "AuthenticationBloc" code completely? Also tell me where do you dispatch Authentication events? – Alireza Abiri Feb 13 '21 at 07:15
  • 1
    Can you list your entire widget tree? I'm wondering if your first widget (the one containing the Listener) isn't actually being rendered (say, because you have an if/else somewhere in the tree that means it's not actually being shown at the time the event is fired. You can check this with the Flutter inspector. Otherwise, is it running in the same BuildContext? If you showed the whole widget tree, this would be easier to diagnose. – Nick Fisher Feb 13 '21 at 09:55
  • @AlirezaAbiri Code added, an event gets added when a new User comes down the User stream: _authenticationRepository.userStream.listen( (user) => add(AuthenticationUserChanged(user)) – Niek Feb 15 '21 at 08:45
  • @NickFisher That was indeed the problem. Thank you so much. If you add it as an answer I can accept it as the solution. The navigation removed the page with the listener from the tree with pushAndRemoveUntil – Niek Feb 15 '21 at 08:54
  • A bit off topic, but can you please tell what is the purpose of Authentication state unknown, i saw it in flutter_bloc docs also. I couldn't understand what is its purpose. – Sanketh B. K Mar 27 '22 at 19:21
  • @sankethB.K As I'm no longer working on this project (internship), I'm not 100% certain but I believe it was used when you first startup the app and you're data has not yet been received from the database (in our case firebase). While it's still unknown you can eg. display a loading screen – Niek Mar 28 '22 at 20:21

3 Answers3

3

Can you list your entire widget tree?

I'm wondering if your first widget (the one containing the Listener) isn't actually being rendered (say, because you have an if/else somewhere in the tree that means it's not actually being shown at the time the event is fired).

You can check this with the Flutter inspector. Otherwise, is it running in the same BuildContext?

If you showed the whole widget tree, this would be easier to diagnose.

Nick Fisher
  • 725
  • 5
  • 13
2

Without seeing the BLoC it's hard to tell but is it possible that you are creating the listener after the state been emitted. Bare in mind that Listener will not fire the last state, only the states been emitted after the creation of the listener.

Please share the BLoC code.

YoBo
  • 2,292
  • 3
  • 9
  • 25
  • Added. As I mentioned before, the first time the state changes from unknown to unauthenticated, it does catch the change and routes to the login page. If you then login and the state changes to authenticated, nothing happens. – Niek Feb 15 '21 at 08:43
  • Can you show class User? You can try to remove Equatable from the state just to be sure that you are not generating the same one(listener will not fire with the same state twice). – YoBo Feb 15 '21 at 09:27
0

It looks like your AuthenticationRepository dispatches the AuthenticationEvent by listening to _authenticationRepository.userStream. I think the problem is that you create an instance of AuthenticationRepository earlier than AuthenticationBloc, this may cause the AuthenticationStatus change before the BLoC strats listening to it.

Though it would be better if you could provide AuthenticationRepository code.

Alireza Abiri
  • 512
  • 4
  • 11