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
- 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
As @offworldwelcome pointed out, the
close()
method is called when explicitly disposing of the AuthBloc instance. This can happen in different scenarios.Manual Disposal: Calling
close()
on it when it's no longer needed, theclose()
method will be executed, and the_userSubscription
will be canceled.Navigation Pop: When using the
AuthBloc
in a widget that is part of a navigation stack (e.g., pushed onto aNavigator
), theclose()
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.Scoped Bloc: When using a coped dependency injection library like
flutter_bloc
, which provides automatic disposal of blocs based on the widget's lifecycle, theclose()
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 theHomePage()
. As a result, LoginCubit is automatically closed.My solution involves refraining from calling
await _authenticationRepository.createUserEntry();
insidelogInWithGoogle()
. Instead, after successfully authenticating and routing users toNavigationPage()
, addcontext.read<AuthenticationRepository>().createUserEntry();
withinNavigationPage()
.Answer based on the comments.
Instead of creating your
LoginCubit
through theBlocProvider.create
method, instantiate it in the same parent widget (level) that contains yourAuthBloc
. Later, whenLoginCubit
is no longer needed, simply callclose
on the instance manually.Here’s a rough example on how to set it up to stop the disposal from happening:
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.