How to define a GoRouter that depends on a Provider?

328

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.

Share:
328
Marco
Author by

Marco

Updated on November 26, 2022

Comments

  • Marco
    Marco over 1 year

    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?

    • Ruchit
      Ruchit about 2 years
      seems right because i was getting a route at runtime and navigating to it works fine, so this should also work.(i am not totally sure because i was only changing my routes once). if this does not gives you compile time error then you should test extensively in runtime if you are not confidence :)