skip to Main Content

I’m using the flutter_firebase_login example from the official GitHub repository of flutter_bloc. My goal is to store supplementary user data in the Firestore database after a successful Google login, specifically for first-time users, in addition to their email, displayName, and photoURL.

Specifically, I’m storing the following fields in the user collection.

  • firstName
  • lastName
  • email
  • title
  • company
  • location
  • bio
  • interests
  • profilePicture

With that said, the exception I encountered originated from the following code snippet, but despite that, I was still able to reach the home screen.

Future<void> logInWithGoogle() async {
  emit(state.copyWith(status : FormzSubmissionStatus.inProgress));
  try {
    await _authenticationRepository.logInWithGoogle();
    
    // NOTE: I added `createUserEntry` on top of the existing code 
    await _authenticationRepository.createUserEntry();

    emit(state.copyWith(status : FormzSubmissionStatus.success));
  }
  on LogInWithGoogleFailure catch (e) {
    emit(state.copyWith(errorMessage
                        : e.message, status
                        : FormzSubmissionStatus.failure, ), );
  }
  catch (_) {
    // NOTE: I got the exception here
    emit(state.copyWith(status : FormzSubmissionStatus.failure));
  }
}

I kept the rest of the code the same as in flutter_firebase_login; the only addition I made was introducing this method:

Future<void> createUserEntry() async {
  final user = _firebaseAuth.currentUser;
  final CollectionReference usersCollection =
      FirebaseFirestore.instance.collection('user');

  final DocumentReference userDocRef = usersCollection.doc(user !.uid);
  final DocumentSnapshot userDoc = await userDocRef.get();

  if (userDoc.exists) {
    // User entry already exists, no need to create a new one.
    return;
  }

  final nameParts = user.displayName !.split(' ');
  await userDocRef.set({
    'firstName' : nameParts.first,
    'lastName' : nameParts.last,
    'email' : user.email,
    'title' : '',
    'company' : '',
    'location' : null,
    'bio' : '',
    'interests' : null,
    'profilePicture' : null,
  });
}

Then, I call the method inside logInWithGoogle() after await _authenticationRepository.logInWithGoogle();

Interestingly enough, without createUserEntry, it successfully emits FormzSubmissionStatus.success, but when included, it does not.

Here are the logs:

[log] Change { currentState: LoginState(FormzInput<String, EmailValidationError>.pure(value: , isValid: false, error: EmailValidationError.invalid), FormzInput<String, PasswordValidationError>.pure(value: , isValid: false, error: PasswordValidationError.invalid), FormzSubmissionStatus.initial, false, null), nextState: LoginState(FormzInput<String, EmailValidationError>.pure(value: , isValid: false, error: EmailValidationError.invalid), FormzInput<String, PasswordValidationError>.pure(value: , isValid: false, error: PasswordValidationError.invalid), FormzSubmissionStatus.inProgress, false, null) }
[log] AuthBloc Instance of ‘_AppUserChanged’
[log] Transition { currentState: AuthState(AuthStatus.unauthenticated, User(null, , null, null)), event: Instance of ‘_AppUserChanged’, nextState: AuthState(AuthStatus.authenticated, User([email protected], MyID, My Name, My Picture)) }
[log] Change { currentState: AuthState(AuthStatus.unauthenticated, User(null, , null, null)), nextState: AuthState(AuthStatus.authenticated, User([email protected], MyID, My Name, My Picture)) }
2
[log] LoginCubit Bad state: Cannot emit new states after calling close

Here is the UI code that corresponds to the state emitted by the LoginCubit:

class LoginForm extends StatelessWidget {
  const LoginForm({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocListener<LoginCubit, LoginState>(
      listener: (context, state) {
        if (state.status.isFailure) {
          ScaffoldMessenger.of(context)
            ..hideCurrentSnackBar()
            ..showSnackBar(
              SnackBar(
                content: Text(state.errorMessage ?? 'Authentication 
                           Failure'),
              ),
            );
        }
      },
      child: Align(
        alignment: const Alignment(0, -1 / 3),
        child: SingleChildScrollView(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              SvgPicture.asset(
                'assets/images/welcome.svg',
                height: 200.h,
              ),
              SizedBox(height: spaceXL.h),
              _EmailInput(),
              SizedBox(height: spaceS.h),
              _PasswordInput(),
              SizedBox(height: spaceM.h),
              _LoginButton(),
              SizedBox(height: spaceM.h),
              const Text('Or'),
              SizedBox(height: spaceM.h),
              _GoogleLoginButton(),
              SizedBox(height: spaceS.h),
              _SignUpButton(),
            ],
          ),
        ),
      ),
    );
  }
}

class _GoogleLoginButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MyButton.secondary(
      key: const Key('loginForm_googleLogin_raisedButton'),
      label: 'SIGN IN WITH GOOGLE',
      icon: Image.asset(
        'assets/images/google_logo.png',
        height: 25.h,
        fit: BoxFit.cover,
      ),
      onPressed: () => context.read<LoginCubit>().logInWithGoogle(),
    );
  }
}

Here is the code with BlocProvider:

class LoginPage extends StatelessWidget {
  const LoginPage({super.key});

  static Page<void> page() => const MaterialPage<void>(child: LoginPage());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(spaceL),
        child: BlocProvider(
          create: (_) => 
                LoginCubit(context.read<AuthenticationRepository>()),
          child: const LoginForm(),
        ),
      ),
    );
  }
}

Could someone shed some light on what might be wrong with my code?

2

Answers


  1. Chosen as BEST ANSWER

    As @offworldwelcome pointed out, the close() method is called when explicitly disposing of the AuthBloc instance. This can happen in different scenarios.

    1. Manual Disposal: Calling close() on it when it's no longer needed, the close() method will be executed, and the _userSubscription will be canceled.

    2. Navigation Pop: When using the AuthBloc in a widget that is part of a navigation stack (e.g., pushed onto a Navigator), the close() method might be called when the widget is popped from the navigation stack. This usually happens automatically when the user navigates back from a screen.

    3. Scoped Bloc: When using a coped dependency injection library like flutter_bloc, which provides automatic disposal of blocs based on the widget's lifecycle, the close() method will be called when the widget is disposed of.

    In my case, as soon as users are authenticated, they are immediately routed to NavigationPage.page(), which in turn leads them to the HomePage(). As a result, LoginCubit is automatically closed.

    My solution involves refraining from calling await _authenticationRepository.createUserEntry(); inside logInWithGoogle(). Instead, after successfully authenticating and routing users to NavigationPage(), add context.read<AuthenticationRepository>().createUserEntry(); within NavigationPage().


  2. Answer based on the comments.

    Instead of creating your LoginCubit through the BlocProvider.create method, instantiate it in the same parent widget (level) that contains your AuthBloc. Later, when LoginCubit is no longer needed, simply call close on the instance manually.

    Here’s a rough example on how to set it up to stop the disposal from happening:

    /// Mock authentication Bloc.
    class AuthBloc extends Bloc<Object, Object> {
      AuthBloc() : super(Object());
    }
    
    /// Mock LoginCubit.
    class LoginCubit extends Cubit<Object> {
      LoginCubit() : super(Object());
    }
    
    
    /// Mock LoginPage.
    class LoginPage extends StatelessWidget {
      const LoginPage({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Container();
      }
    }
    
    /// This widget is placed above any other widget that consumes your Blocs.
    class MyParent extends StatefulWidget {
      const MyParent({Key? key}) : super(key: key);
    
      @override
      State<MyParent> createState() => _MyParentState();
    }
    
    class _MyParentState extends State<MyParent> {
      
      final _authBloc = AuthBloc();
      final _loginCubit = LoginCubit();
    
      @override
      void dispose() {
        /// First check if they're already closed, just in case they were closed
        /// elsewhere. This is only needed because the 'BlocProvider's do not
        /// manage the lifetime of these Blocs.
        /// 
        /// They don't manage the lifetime of these blocs because they are created
        /// with the 'BlocProvider.value' constructor, which means that the blocs
        /// are not created by the 'BlocProvider's, but rather passed to them.
        if (!_authBloc.isClosed) _authBloc.close();
        if (!_loginCubit.isClosed) _loginCubit.close();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return MultiBlocProvider(
          providers: [
            /// Use the BlocProvider.value constructor.
            BlocProvider.value(value: _authBloc),
            BlocProvider.value(value: _loginCubit),
          ],
    
          /// This is your login page widget, it contains the builders that rebuild
          /// based on the states emitted by the blocs.
          child: const LoginPage(),
        );
      }
    }
    

    As soon as you no longer need a bloc (i.e. LoginCubit), then simply close it manually:

    context.read<LoginCubit>().close();

    Let me know if refactoring your bloc lifetime this way solves your issue.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search