Flutter bloc – emit was called after an event handler completed normally

Issue

I have this code in user_bloc.dart:

UserBloc(this._userRepository, UserState userState) : super(userState) {
    on<RegisterUser>(_onRegisterUser);
  }

void _onRegisterUser(RegisterUser event, Emitter<UserState> emit) async {
    emit(UserRegistering());
    try {
      // detect when the user is registered
      FirebaseAuth.instance.authStateChanges().listen((User? user) async {
        if (state is UserRegistering && user != null) {
          // save user to db
          try {
            await _userRepository.addUpdateUserInfo(user.uid, userInfo);
          } catch (e) {
            emit(UserRegisteringError("Could not save user"));
          }
          emit(UserLoggedIn(user));
        }
      });
       ... call FirebaseAuth.instance.createUserWithEmailAndPassword

this is _userRepository.addUpdateUserInfo:

Future<void> addUpdateUserInfo(String userId, UserInfo userInfo) async {
    try {
      var doc = usersInfo.doc(userId);
      await doc.set(
        {
          'first_name': userInfo.firstName,
          'last_name': userInfo.lastName,
          'email': userInfo.email
        },
        SetOptions(merge: true),
      );
    } catch (e) {
      print("Failed to add user: $e");
      throw Exception("Failed to add user");
    }
  }

When emit(UserLoggedIn(user)); is called I get this error:

Error: Assertion failed:
..\…\src\emitter.dart:114
!_isCompleted
"\n\n\nemit was called after an event handler completed normally.\nThis is usually due to an unawaited future in an event handler.\nPlease make sure to await all asynchronous operations with event handlers\nand use emit.isDone after asynchronous operations before calling emit() to\nensure the event handler has not completed.\n\n  **BAD**\n  on<Event>((event, emit) {\n    future.whenComplete(() => emit(...));\n  });\n\n  **GOOD**\n  on<Event>((event, emit) async {\n    await future.whenComplete(() => emit(...));\n  });\n"
    at Object.throw_ [as throw] (http://localhost:58334/dart_sdk.js:5067:11)
    at Object.assertFailed (http://localhost:58334/dart_sdk.js:4992:15)
at _Emitter.new.call (http://localhost:58334/packages/bloc/src/transition.dart.lib.js:765:40)
at user_bloc.UserBloc.new.<anonymous> (http://localhost:58334/packages/soli/blocs/bloc/user_bloc.dart.lib.js:90:20)
    at Generator.next (<anonymous>)
    at http://localhost:58334/dart_sdk.js:40571:33
    at _RootZone.runUnary (http://localhost:58334/dart_sdk.js:40441:59)
    at _FutureListener.thenAwait.handleValue (http://localhost:58334/dart_sdk.js:35363:29)
    at handleValueCallback (http://localhost:58334/dart_sdk.js:35931:49)
    at Function._propagateToListeners (http://localhost:58334/dart_sdk.js:35969:17)
    at _Future.new.[_completeWithValue] (http://localhost:58334/dart_sdk.js:35817:23)
    at async._AsyncCallbackEntry.new.callback (http://localhost:58334/dart_sdk.js:35838:35)
    at Object._microtaskLoop (http://localhost:58334/dart_sdk.js:40708:13)
    at _startMicrotaskLoop (http://localhost:58334/dart_sdk.js:40714:13)
    at http://localhost:58334/dart_sdk.js:36191:9
[2022-02-17T03:47:00.057Z]  @firebase/firestore:

After I get the error, if I try again with a different user, it works OK.

Solution

This is the expected outcome since the _onRegisterUser method has already finished executing but the code inside FirebaseAuth.instance.authStateChanges().listen(...) is trying to emit state changes afterwards.

What you can do in this case, instead of emitting new states inside the FirebaseAuth listener, you should add new events to the BLoC:

if (state is UserRegistering && user != null) {
  // save user to db
  try {
    await _userRepository.addUpdateUserInfo(user.uid, userInfo);
  } catch (e) {
    add(RegistrationErrorEvent()); // Create this event
  }
    add(UserLoggedInEvent(user: user)); // Create this event
  }

Then, you should register these events and handle the logic:

UserBloc(this._userRepository, UserState userState) : super(userState) {
  on<RegisterUser>(_onRegisterUser);
  on<RegistrationErrorEvent>((event, emit) => emit(UserRegisteringError("Could not save user")));
  on<UserLoggedInEvent>((event, emit) => emit(UserLoggedIn(event.user)));
}

Also, since you are using streams and the listen method, consider using StreamSubscription so you could clean up the resources after listening for the state changes.

Here is an example from the bloc repository on GitHub.

Answered By – mkobuolys

Answer Checked By – Katrina (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published.