Как определить GoRouter, зависящий от провайдера?

Я интегрирую GoRouter в свое приложение Flutter, где уже использую Riverpod. у меня есть isAuthorizedProviderопределяется следующим образом:

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

И я не уверен, как определить GoRouter, который зависит от указанного выше провайдера. Я придумал следующее:

      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,
    );
  }

Это правильный способ сделать это?

2 ответа

Я не думаю, что вы должны звонить по этой линии

      ref.watch(isAuthorizedProvider);

внутри блока перенаправления, потому что это приведет к тому, что весь ваш GoRouterinstance для перестроения (и вы потеряете весь стек навигации).

Вот как я это сделал:

      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: [],
  );
}

Обратите внимание, что этот код не является реактивным в том смысле, что он будет обновлять маршрутизатор при изменении authState. Таким образом, в сочетании с этим вам необходимо выполнить явное событие навигации при входе/выходе.

В качестве альтернативы вы можете использовать refreshListenableаргумент.

Вы можете сделать это таким образом, используя перенаправление, однако я придумал способ, который использует . Таким образом, вы сохраняете исходное состояние навигатора (вы будете перенаправлены обратно на ту страницу, которую вы изначально посетили в Интернете или с прямой ссылкой), и весь маршрутизатор не нужно постоянно перестраивать.

      final routerProvider = Provider((ref) {
  return GoRouter(
    routes: [
      GoRoute(
        path: '/',
        builder: (context, state) => const OrdersScreen(),
      ),
      GoRoute(
        path: '/login',
        builder: (context, state) => const AuthScreen(),
      ),
    ],
    navigatorBuilder: (context, state, child) {
      return Consumer(
        builder: (_, ref, __) =>
          ref.watch(authControllerProvider).asData?.value == null
            ? Navigator(
                onGenerateRoute: (settings) => MaterialPageRoute(
                  builder: (context) => AuthScreen(),
                ),
              )
            : child,
      );
    },
  );
});

в основном позволяет вам вставлять некоторые виджеты между MaterialApp и Navigator. Мы используем потребительский виджет Riverpod для доступа к ref, и тогда весь маршрутизатор не нужно перестраивать, и мы можем получить доступ к состоянию аутентификации, используя ref.

В моем примеревозвращает, поэтому, если пользователь вошел в систему, мы возвращаем(текущий маршрут навигации), если они вышли из системы, покажите им экран входа в систему, а если они загружаются, мы можем показать экран загрузки и т. д.

Если вы хотите перенаправить пользователей на основе ролей (например, только администратор может видеть панель администратора), то эта логика должна войти в функцию перенаправления с использованием слушаемого, как описано @bizz84.

Другие вопросы по тегам