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?
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.
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.
GoRouter
’s refreshListenable:
parameterThis 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.
GoRouter
’s .refresh()
methodA 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 !