So I have a flutter app which uses Bloc pattern. I'm still kind of new with how Blocs works compared to Cubits. But I get the general idea (events vs functions).
The problem is with an async process in the login flow, I'm unable to make my View wait for the whole async process, which involves an extra API call (inside a StreamSubscription.listen({//api call here})
) after the user is authenticated with firebase.
Classes:
So, I have a button that triggers the authentication process:
class View extends StatefulWidget {
const View({super.key});
@override
State<View> createState() => _ViewState();
}
class _ViewState extends State<View> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LoginCubit(),
child: ElevatedButton(
child: Text('Click Me'),
onPressed: () async {
context.loaderOverlay.show();
await context.read<LoginCubit>().loginWithEmailAndPassword(context);
context.loaderOverlay.hide();
},
),
);
}
}
This View class has it's own Cubit (not bloc) to handle the onPressed callback of the ElevatedButton
:
class LoginCubit extends Cubit<LogInState> {
LoginCubit(this._authenticationRepository) : super(const LogInState());
final AuthenticationRepository _authenticationRepository;
Future<void> loginWithEmailAndPassword(BuildContext context) async {
var emitNavigation = false;
try {
//After the following call is completed, _AppUserChanged Bloc event is triggered to set user
//info in AppState class
await _authenticationRepository
.logInWithEmailAndPassword(
email: 'em@il.com', password: 'Password')
// This does not waits for extra API call of the _userSubcription stream .listen
emit(state.copyWith(
loginStatus: FormzStatus.submissionSuccess,
successMessage:
'${LocaleKeys.successSignInMessage.tr()}${context.read<AppBloc>().state.userDocument.fullName}'));
});
} on LogInWithEmailAndPasswordFailure catch (error) {
// ...
} catch (_) {
// ...
}
}
}
So as one of the comment mentions, the emit
with the successful message does not waits for the userDocument
model to be queried and mapped. I can tell because the message has NULL as fullname or just gives an error if I apply null check to it. The async process that creates the userDocument is defined as an event on the .listen()
of the StreamSubscription
defined in the AppBloc class:
class AppBloc extends Bloc<AppEvent, AppState> {
AppBloc({required AuthenticationRepository authenticationRepository})
: _authenticationRepository = authenticationRepository,
super(
authenticationRepository.currentUser.isNotEmpty
? AppState.authenticated(
authenticationRepository.currentUser, null)
: const AppState.unauthenticated(),
) {
on<_AppUserChanged>(_onUserChanged);
on<_AppUserDocumentChanged>(_onUserDocumentChanged); // Event definition
on<AppLogoutRequested>(_onLogoutRequested);
_userSubscription = _authenticationRepository.user.listen(
(user) {
add(_AppUserChanged(user));
// -> userDocument creation event, needs the user id of the just authenticated user
add(_AppUserDocumentChanged(user.id));
},
);
}
final AuthenticationRepository _authenticationRepository;
late final StreamSubscription<FirebaseAuthUser> _userSubscription;
void _onUserChanged(_AppUserChanged event, Emitter<AppState> emit) {
emit(
event.user.isNotEmpty
? AppState.authenticated(event.user, null)
: const AppState.unauthenticated(),
);
}
Future<void> _onUserDocumentChanged(
_AppUserDocumentChanged event, Emitter<AppState> emit) async {
if (event.userId.isEmpty) {
const AppState.unauthenticated();
} else {
try {
// -> The aysnc flow works fine until it runs the following line, when this is
// executed, the flow continues with the emit in the LoginCubit wihout waiting for the next two lines to be executued (which I need)
final userInfo =
await _authenticationRepository.getUserDocumentById(event.userId);
emit(AppState.authenticated(state.user, userInfo));
} catch (e) {
print(e);
}
}
}
void _onLogoutRequested(AppLogoutRequested event, Emitter<AppState> emit) {
unawaited(_authenticationRepository.logOut());
}
@override
Future<void> close() {
_userSubscription.cancel();
return super.close();
}
}
Just read the couple comments detailed in this. I also tried to implement this in single event, like using the _onUserChanged
event for the whoel process but it did not work as well. Any idea of how to fix this or what am I doing wrong?
Here's the actual authentication authenticationRepository method:
Future<void> logInWithEmailAndPassword({
required String email,
required String password,
}) async {
try {
// this triggers bloc event _AppUserChanged
await _firebaseAuth.signInWithEmailAndPassword(
email: email,
password: password,
);
} on firebase_auth.FirebaseAuthException catch (e) {
// ...
} catch (_) {
// ...
}
}
and the user
stream getter:
Stream<FirebaseAuthUser> get user {
return _firebaseAuth.authStateChanges().asyncMap((firebaseUser) {
final user =
firebaseUser == null ? FirebaseAuthUser.empty : firebaseUser.toUser;
_cache.write(key: userCacheKey, value: user);
return user;
});
}
UPDATE
@Boseong 's answer (ideal method) worked, but there's another problem. In the scenario of having an active session and rebuilding the app, I would need to redo the same async call in order to map userDocument
.
It's not recommended that use BuildContext in BLoC (or Cubit). Because BLoC is a Business Logic which is not related with render tree.
So, try use repository to sync data, or sync data in render(widget) tree.
await _authenticationRepository.logInWithEmailAndPassword(...);
final user = await _authenticationRepository.user.first;
final userDocument = await _authenticationRepository.getUserDocumentById(user.id);
final message = LocaleKeys.successSignInMessage.tr();
final fullName = userDocument.fullName;
emit(state.copyWith(
loginStatus: FormzStatus.submissionSuccess,
successMessage: '$message$fullName',
));
if you want to use BuildContext in LoginCubit, wait until AppState in AppBloc changed authenticated with UserDocument property.
sample code
await _authenticationRepository
.logInWithEmailAndPassword(
email: 'em@il.com', password: 'password');
await context.read<AppBloc>.stream.firstWhere(
(state) => state.userInfo != null,
);
final message = LocaleKeys.successSignInMessage.tr();
final fullName = context.read<AppBloc>().state.userDocument.fullName;
emit(state.copyWith(
loginStatus: FormzStatus.submissionSuccess,
successMessage: '$message$fullName',
));