Flutter Firebase Auth + Firestore Provider user document

Viewed 293

A common scenario in nearly every single Flutter Firebase project is to use authStateChanges to determine logged in/out state by checking if the User object is null. But generally there are extra fields that are needed on a user, which come from Firestore. (I usually make a FirestoreUser model). It's generally really nice to be able to stream the current user's Firestore document and have it available throughout the app. But there's a few issues.

final user = context.read<User?>();

return StreamProvider<FirestoreUser?>(
  create: (_) => UserRepository(FirebaseFirestore.instance)
      .streamUser(user!.uid),
  initialData: null,
  child: const HomeScreen(),
);

StreamProvider's require an initialData. The end goal would be to use context.watch<FirestoreUser>() throughout the app, but since User (FirebaseAuth user) can be null, FirestoreUser (Firestore user) also has to be null.

final user = context.watch<FirestoreUser?>();
// this should technically never happen, but still need to check for it because it's impossible(?) to Provide a non-null FirestoreUser
if(user == null) return SizedBox.shrink();
...
return Text('Hello ${user.displayName}');

It would be super annoying if I had to do this on every. single. page. (This is going to be a large application)

The FirestoreUser should be available throughout the app, so it needs to be above MaterialApp. This makes the logged in/out logic complicated, since your login screen needs to be below MaterialApp.

I am also using MaterialApp.router using the Beamer package. They have a bunch of examples, but none of them are actually useful. Their FirebaseAuth example has everything in one widget and would never be a real world scenario.

Also, it's been a good while since null-safety was introduced but there are so few examples/tutorials out there that actually implement null-safety.

I'm convinced it is impossible to listen to authStateChanges to control logged in/out state and then only providing a non-null Firestore document to use throughout the application using MaterialApp.router. Is that asking too much? I feel like this would be the most common scenario. It's one I've been struggling with for my past 5+ apps.

1 Answers

I implemented the same scenario with MaterialApp.router, I hope my solution helps you.

I set up a ChangeNotifierProvider to handle authentication state called MyAuthState. MyAuthState is placed above MaterialApp.router, I actually use more providers, so it looks like the following. For the navigation to be accessibly (e.g. route the user to login etc.) I add the RouterDelegate to the ChangeNotifierProvider:

MultiProvider(
  providers: [
    ChangeNotifierProvider(
        create: (context) => MyAuthState(
            routerDelegate: ...)),
    ...
  ],
  builder: (context, child) {
    return MaterialApp.router(
      ...
    );
  }

Then in MyAuthState I listen to Firebase Authentication state changes, save current authentication state and start the stream to listen to my own Firestore user collection if a user is authenticated. Something like:

StreamSubscription<MyUser?>? _myUserStream;

...

FirebaseAuth.instance.authStateChanges().listen((User? firebaseUser) {
  // save authentication state to know whether a user is authenticated
  // if it is not null, start a stream

  _myUserStream = <stream to listen snapshots for my own user collection>;
  
  notifyListeners();
});

After this I use an authentication router as the home route of my application, consuming MyAuthState provider, and this will decide whether to show a login screen or something else (the code uses an enum for the different states):

class _MyAuthRouter extends State<MyAuthRouter> {
  @override
  Widget build(BuildContext context) {
    return Consumer<MyAuthState>(builder: (context, myAuthState, child) {
      switch (myAuthState.status) {

        // before first auth state => progress indicator
        case MyAuthStatus.unknown:
          return const CircularProgressIndicator();

        // user is logged out, not found or disabled => login screen
        case MyAuthStatus.loggedOut:
        case MyAuthStatus.notFound:
        case MyAuthStatus.disabled:
          return const MyAuthLogin();

        // email not verified => verify email screen
        case MyAuthStatus.emailUnverified:
          return const MyAuthVerifyEmail();

        // logged in => home screen
        case MyAuthStatus.loggedIn:
          return const MyPageHome();
      }
    });
  }
}

And since I only get to MyPageHome upon user is logged in, I know that the stream for my user collection will be available and I can use context.read or context.watch to get the user's data.

Related