Search code examples
flutterfirebase-authenticationflutter-navigationflutter-go-routergorouter

How to handle authentication in Flutter with go_router and Firebase Authentication when already on a route?


In my Flutter application, I'm using the go_router package to manage routes and Firebase Authentication for user authentication. I have several screens that require users to be authenticated to access, such as account management, transactions, and details.

Currently, I have implemented redirects with go_router to ensure that unauthenticated users are redirected correctly. However, I face a challenge when the user is already on one of these screens and logs out or the session expires.

I'm considering using a BlocListener to detect changes in the authentication state on each screen, but this seems to lead to code duplication. I also have a Stream that notifies changes in the authentication state, thanks to Firebase Authentication, and updates the contex.isUserSignIn variable.

What would be the best practice for handling logout or session expiration events in Flutter with go_router and Firebase Authentication efficiently?


Solution

  • I’ve been struggling with the exact same question and have researched several alternatives and options.

    TL;DR: Option 3 is my preferred choice, which uses the GoRouter.refresh() method at the main() level to dynamically update the GoRouter state based on events from the auth stream.

    Option 1: Follow the go_router async_redirection.dart example

    See here for the example: https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/async_redirection.dart

    This wraps the top level app, typically MyApp (as returned by main() in runApp() ) in an InheritedNotifer widget, which they call StreamAuthScope and which creates a dependency between the notifier StreamAuthNotifier and go_router's parsing pipeline. This in turn will rebuild MyApp (or App in the example) when the auth status changes (as communicated by StreamAuthNotifier via notifyListeners()).

    I implemented a similar model based on the Provider package where the ChangeProviderNotifier replaces StreamAuthScope and wraps the top level MyApp returned by main(). However this doesn’t allow the creation of a monitored Provider.of<> inside the GoRouter( redirect: ) enclosure. To solve this I created a getRouter function that passed in isUserSignIn which was monitored with a Provider.of<> in the main body of MyApp but before the build function. This works but feels cumbersome and causes the main MyApp to be rebuilt each time auth status changes. If desired, I’m sure you could do something similar with a BLoC model in place of Provider.

    Option 2: Use GoRouter’s refreshListenable: parameter

    This is based on this go_router redirection.dart example: https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/redirection.dart

    In the question you mentioned you have a stream that notifies auth state changes. You can wrap this in a class with extends ChangeNotifier to make it Listenable. Then in the constructor you can instantiate and monitor the stream with .listen, and in the enclosure issue a notifyListerners() each time there is an auth state change (probably each time there is a stream event). In my case I called this class AuthNotifier This can then be used as the listenable with GoRouter’s refreshListenable: parameter simply as: refreshListenable: AuthNotifier()

    Example AuthNotifier class

    class AuthNotifier extends ChangeNotifier {
      AuthNotifier() {
        // Continuously monitor for authStateChanges
        // per: https://firebase.google.com/docs/auth/flutter/start#authstatechanges
        _subscription =
            FirebaseAuth.instance.authStateChanges().listen((User? user) {
              // if user != null there is a user logged in, however
              // we can just monitor for auth state change and notify
              notifyListeners();
            });
      } // End AuthNotifier constructor
    
      late final StreamSubscription<dynamic> _subscription;
    
      @override
      void dispose() {
        _subscription.cancel();
        super.dispose();
      }
    }
    

    Note: to avoid multiple streams being created and monitored, you need to ensure this constructor is only called once in your app (in this case as part of GoRouter’s refreshListenable:), or else modify it to be a singleton.

    Option 3: Use GoRouter’s .refresh() method

    A similar, but more direct approach to option 2 is to use GoRouter’s .refresh() method. This directly calls an internal notifyListerners() that refreshes the GoRouter configuration. We can use a similar class to the AuthNotifier above but we don’t need extends ChangeNotifier and would call router.refresh() in place of notifyListeners(), where router is your GoRouter() configuration. This new class would be instantiated in main().

    Given its so simple (2-3 lines of code), we can also skip the class definition and instantiation and implement the functionality directly in the main() body, as follows:

    Future<void> main() async {
      WidgetsFlutterBinding.ensureInitialized();
      await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
    
      // Listen for Auth changes and .refresh the GoRouter [router]
      FirebaseAuth.instance.authStateChanges().listen((User? user) {
        router.refresh();
      });
    
      runApp(
        const MyApp(),
      );
    }
    

    Since this appears to be the most direct and simplest solution, it is my preferred solution and the one I have implemented. However there is a lot of confusing and dated information out there and I don’t feel I have enough experience to claim it as any sort of 'best practice', so will leave that for others to judge and comment.

    I hope all this helps you and others as it’s taken me a long time with work out these various options and wade through the wide range of materials and options out there. I feel there is a definitely an opportunity to improve the official go_router documentation in this area !