Search code examples
flutterriverpodflutter-go-router

Flutter read riverpod provider in GoRoute builder


My Flutter project is migrating to go_router and I have trouble understanding how to access riverpod providers in either a GoRoute's build method or redirect method, as I need to access the user's data to control the navigation.

I have a top-level redirect that checks if a user is logged in and sends them to a LoginPage if not. All users can access so-called activities in my app, but only admins can edit them. Whether a user is an admin is stored in a riverpod userDataProvider, which always contains a user if the user is logged in. Now if a user attempts to enter the route /:activityId?edit=true, I want to check whether they are allowed to by accessing the userDataProvider. However, I do not see what the clean way of accessing this provider is.

I found somewhere (can't find the thread anymore), that one way is to use ProviderScope.containerOf(context).read(userDataProvider), but I have never seen this before and it seems a bit exotic to me. Is this the way to go?

My GoRoute looks something like this:

GoRoute(
  path: RouteName.event.relPath,
  builder: (context, state) {
    final String? id = state.params['id'];
    final bool edit = state.queryParams['edit'] == 'true';

    if (state.extra == null) {
      // TODO: Fetch data
    }

    final data = state.extra! as Pair<ActivityData, CachedNetworkImage?>;

    if (edit) {
      return CreateActivity(
        isEdit: true,
        data: data.a,
        banner: data.b,
      );
    }

    return ActivityPage(
      id: id!,
      data: data.a,
      banner: data.b,
    );
  },
  redirect: (context, state) {
    final bool edit = state.queryParams['edit'] == 'true';
    if (edit) {
      // IMPORTANT: How to access the ref here?
      final bool isAdmin =
          ref.read(userDataProvider).currentUser.customClaims.admin;
      if (isAdmin) {
        return state.location; // Includes the queryParam edit
      } else {
        return state.subloc; // Does not include queryParam
      }
    } else {
      return state.path;
    }
  },
),

Solution

  • In my current application, I used something similar approach like this :

    Provider registration part (providers.dart) :

        final routerProvider = Provider<GoRouter>((ref) {
          final router = RouterNotifier(ref);
          return GoRouter(
              debugLogDiagnostics: true,
              refreshListenable: router,
              redirect: (context, state) {
                router._redirectLogic(state);
                return null;
              },
              routes: ref.read(routesProvider));
        });
        
        class RouterNotifier extends ChangeNotifier {
          final Ref _ref;
        
          RouterNotifier(this._ref) {
            _ref.listen<AuthState>(authNotifierProvider, (_, __) => notifyListeners());
          }
        
          String? _redirectLogic(GoRouterState state) {
            final loginState = _ref.watch(authNotifierProvider);
        
            final areWeLoggingIn = state.location == '/login';
        
            if (loginState.state != AuthenticationState.authenticated) {
              return areWeLoggingIn ? null : '/login';
            }
        
            if (areWeLoggingIn) return '/welcome';
        
            return null;
          }
        }
    

    Main app building as router (app.dart):

        class App extends ConsumerWidget {
          const App({Key? key}) : super(key: key);
        
          // This widget is the root of your application.
          @override
          Widget build(BuildContext context, WidgetRef ref) {
            final GoRouter router = ref.watch(routerProvider);
            return MaterialApp.router(
              routeInformationProvider: router.routeInformationProvider,
              routeInformationParser: router.routeInformationParser,
              routerDelegate: router.routerDelegate,
              debugShowCheckedModeBanner: false,
              title: 'Flutter Auth',
            }
          }
        }
    

    And as entrypoint (main.dart):

        Future<void> main() async {
          F.appFlavor = Flavor.dev;
          WidgetsFlutterBinding.ensureInitialized();
          await setup();
          runApp(ProviderScope(
            observers: [
              Observers(),
            ],
            child: const App(),
          ));
        }