flutterdartriverpodgorouter

How to define a GoRouter that depends on a Provider?


I'm integrating GoRouter in my Flutter app where I'm already using Riverpod. I have an isAuthorizedProvider defined as follows:

final isAuthorizedProvider = Provider<bool>((ref) {
  final authStateChanged = ref.watch(_authStateChangedProvider);
  final user = authStateChanged.asData?.value;
  return user != null;
});

And I'm not sure how to define a GoRouter that depends on the Provider above. I've come up with the following:

final goRouterProvider = Provider<GoRouter>((ref) => GoRouter(
      debugLogDiagnostics: true,
      redirect: (state) {
        final isAuthorized = ref.watch(isAuthorizedProvider);
        final isSigningIn = state.subloc == state.namedLocation('sign_in');

        if (!isAuthorized) {
          return isSigningIn ? null : state.namedLocation('sign_in');
        }

        // if the user is logged in but still on the login page, send them to
        // the home page
        if (isSigningIn) return '/';

        // no need to redirect at all
        return null;
      },
      routes: [
        GoRoute(
          path: '/',
          ...,
        ),
        GoRoute(
          name: 'sign_in',
          path: '/sign_in',
          ...,
        ),
        GoRoute(
            name: 'main',
            path: '/main',
            ...,
        ),
        ...
      ],
    ));

class MyApp extends ConsumerWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final goRouter = ref.watch(goRouterProvider);
    return MaterialApp.router(
      routeInformationParser: goRouter.routeInformationParser,
      routerDelegate: goRouter.routerDelegate,
    );
  }

Is this the right way to do it?


Solution

  • I don't thing you should be calling this line

    ref.watch(isAuthorizedProvider);
    

    inside the redirect block, because that will cause your entire GoRouter instance to rebuild (and you'll lose the entire nav stack).

    This is how I've done it:

    class AppRouterListenable extends ChangeNotifier {
      AppRouterListenable({required this.authRepository}) {
        _authStateSubscription =
            authRepository.authStateChanges().listen((appUser) {
          _isLoggedIn = appUser != null;
          notifyListeners();
        });
      }
      final AuthRepository authRepository;
      late final StreamSubscription<AppUser?> _authStateSubscription;
      var _isLoggedIn = false;
      bool get isLoggedIn => _isLoggedIn;
    
      @override
      void dispose() {
        _authStateSubscription.cancel();
        super.dispose();
      }
    }
    
    final appRouterListenableProvider =
        ChangeNotifierProvider<AppRouterListenable>((ref) {
      final authRepository = ref.watch(authRepositoryProvider);
      return AppRouterListenable(authRepository: authRepository);
    });
    
    final goRouterProvider = Provider<GoRouter>((ref) {
      final authRepository = ref.watch(authRepositoryProvider);
      final appRouterListenable =
          AppRouterListenable(authRepository: authRepository);
      return GoRouter(
        debugLogDiagnostics: false,
        initialLocation: '/',
        redirect: (state) {
          if (appRouterListenable.isLoggedIn) {
            // on login complete, redirect to home
            if (state.location == '/signIn') {
              return '/';
            }
          } else {
            // on logout complete, redirect to home
            if (state.location == '/account') {
              return '/';
            }
            // TODO: Only allow admin pages if user is admin (#125)
            if (state.location.startsWith('/admin') ||
                state.location.startsWith('/orders')) {
              return '/';
            }
          }
          // disallow card payment screen if not on web
          if (!kIsWeb) {
            if (state.location == '/cart/checkout/card') {
              return '/cart/checkout';
            }
          }
          return null;
        },
        routes: [],
      );
    }
    

    Note that this code is not reactive in the sense that it will refresh the router when the authState changes. So in combination with this, you need to perform an explicit navigation event when you sign-in/sign-out.

    Alternatively, you can use the refreshListenable argument.