Use This 1 Line to Get Current User from Any Flutter Widget
People change.
And so do your users. They can log in, log out, update their name and profile photo. And the app’s UI must be updated with the new data in real-time.
So I’ll share with you a scalable way to solve this problem.
After we’re done, you’ll only need 1 line to get the current user and rebuild the UI whenever the user updates:
final user = context.watchCurrentUser;
Clean, right?
The solution uses 3 architecture layers: UI, Bloc, Repository. And we will be using Firebase and flutter_bloc
package.
Here are the 7 steps:
UserState
to store user stateUserCubit
to handle user changesAuthRepository
to listen to user log in/outUserRepository
to listen to user data changesBuildContext
extension functions to keep the code cleanCreate
UserCubit
inmain.dart
Call
watchCurrentUser
in Widgetbuild
method
Let’s go!
Step 1: UserState
We use UserState
to keep the current user’s state.
This is how it looks like:
part of 'user_cubit.dart';
@immutable
abstract class UserState {
const UserState();
}
class UserInitial extends UserState {
const UserInitial();
}
class UserLoaded extends UserState {
final User? user;
const UserLoaded({required this.user});
}
We have the base UserState
and 2 subclasses: UserInitial
(initial value, until the User is loaded) and UserLoaded
(emitted once user is loaded, null
value means user is signed out).
Pro tip: Use
const
constructors to always return the same instance for same param values, and avoid unnecessary UI rebuilds when emitting states in the cubit.
Step 2: UserCubit
Now comes the fun part.
Here’s how our UserCubit
looks like (don’t be scared, we’ll break it down):
part 'user_state.dart';
class UserCubit extends Cubit<UserState> {
final AuthRepository authRepository;
final UserRepository userRepository;
StreamSubscription<String?>? _userIdSubscription;
StreamSubscription<User?>? _userSubscription;
UserCubit({
required this.authRepository,
required this.userRepository,
}) : super(const UserInitial()) {
_userIdSubscription = authRepository.getUserIdStream().listen(_onUserId);
}
@override
Future<void> close() {
_userIdSubscription?.cancel();
_userSubscription?.cancel();
return super.close();
}
void _onUserId(String? userId) {
_userSubscription?.cancel();
if (userId == null) {
emit(const UserLoaded(user: null));
return;
}
_userSubscription = userRepository.getUserStream(userId).listen(_onUser);
}
Future<void> _onUser(User? user) async {
emit(UserLoaded(user: user));
}
}
We have 2 streams: one for user ID, one for user data. First we listen to user ID changes (from the AuthRepository
) and then we use that ID to listen to the user data changes (from the UserRepository
).
On new user data we emit the UserLoaded
state. So any Widget that is listening to UserCubit
changes will be rebuilt.
But what are AuthRepository
and UserRepository
?
I’m glad you asked…
Step 3: AuthRepository
This is how AuthRepository
looks like:
class AuthRepository {
final _firebaseAuth = FirebaseAuth.instance;
Stream<String?> getUserIdStream
() =>
_firebaseAuth.authStateChanges().map((firebaseUser) => firebaseUser?.uid);
}
A simple class with just 1 function: getUserIdStream
which listens to FirebaseAuth
state changes and returns the current user’s ID.
We don’t want to return the user object from FirebaseAuth
, because our user data is kept in the Firestore database.
Step 4: UserRepository
Here’s our UserRepository
class:
class UserRepository {
final _firestore = FirebaseFirestore.instance;
Stream<User?> getUserStream(String userId) {
return _firestore
.collection('users')
.doc(userId)
.snapshots()
.map((snapshot) {
if (!snapshot.exists) {
return null;
}
return User.fromFirestore(snapshot);
});
}
}
We assume user data is stored in Firestore document under /users/$userId
.
As you can see, we don’t use .get()
to fetch the data once, but .snapshots()
that returns the stream of data.
And User.fromFirestore
is a factory constructor that parses the data, like so:
factory User.fromFirestore(
DocumentSnapshot<Map<String, dynamic>> snapshot,
) {
final data = snapshot.data()!;
return User(
id: snapshot.id,
name: data['name'] ,
email: data['email'],
);
}
Step 5: BuildContext extension functions
And now the magic sauce — the BuildContext
extension functions:
extension BuildContextUserExt on BuildContext {
User? get watchCurrentUser {
final userState = watch<UserCubit>().state;
if (userState is! UserLoaded) {
return null;
}
return userState.user;
}
User? get getCurrentUser {
final userState = read<UserCubit>().state;
if (userState is! UserLoaded) {
return null;
}
return userState.user;
}
}
We created 2 functions:
watchCurrentUser
- use it when you want your UI to update on user changes (i.e. in abuild
method of a Widget that shows user data)getCurrentUser
- use it when you just need to read the user once (i.e. inonTap
callback)
These take advantage of flutter_bloc
extensions methods watch
and read
.
Step 6: Create UserCubit in main.dart
This step is simple:
runApp(MultiRepositoryProvider(
providers: [
RepositoryProvider<AuthRepository>(
create: (context) => AuthRepository(),
),
RepositoryProvider<UserRepository>(
create: (context) => UserRepository(),
),
],
child: MultiBlocProvider(
providers: [
BlocProvider<UserCubit>(
create: (context) => UserCubit(
userRepository: context.read<UserRepository>(),
authRepository: context.read<AuthRepository>(),
),
),
],
child: const TopSecretApp(),
),
));
We use MultiRepositoryProvider
to create our 2 repositories, and MultiBlocProvider
to create our UserCubit
.
And replace TopSecretApp
with your MaterialApp
widget.
Step 7: Call watchCurrentUser
Yes, we could use BlocBuilder, BlocConsumer, or even BlocSelector. But look how clean this extension function looks:
@override
Widget build(BuildContext context) {
final user = context.watchCurrentUser;
if (user == null) return const SizedBox();
return Text(user.name);
}
And the best part — this widget will rebuild on user change.
Amazing, right?
Want me to launch your MVP in 4 weeks? Apply here
Otherwise,
You can find me on Linkedin or Twitter, if you'd like to follow my journey. To get an alert when I write something new, click the Subscribe button.