【问题标题】:How to define a GoRouter that depends on a Provider?如何定义依赖于 Provider 的 GoRouter?
【发布时间】:2022-08-15 13:19:22
【问题描述】:

我正在将 GoRouter 集成到我已经在使用 Riverpod 的 Flutter 应用程序中。我有一个isAuthorizedProvider 定义如下:

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

而且我不确定如何定义一个依赖于上面Provider的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,
    );
  }

这是正确的方法吗?

  • 似乎是对的,因为我在运行时获得了一条路线并导航到它工作正常,所以这也应该有效。(我不完全确定,因为我只更改了一次路线)。如果这不会给您带来编译时错误,那么如果您没有信心,您应该在运行时进行广泛测试:)

标签: flutter dart riverpod gorouter


【解决方案1】:

我不认为你应该打电话给这条线

ref.watch(isAuthorizedProvider);

在重定向块内,因为这将导致您的整个 GoRouter 实例重建(并且您将丢失整个导航堆栈)。

这就是我的做法:

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 参数。

【讨论】:

  • 我看不出这将如何与 Firebase 身份验证之类的东西一起使用。因为路由器是被动的,而不是监视的,所以在身份验证系统正在确定您是否实际登录的期间,对appRouterListenable.isLoggedIn 之类的第一次调用将是错误的——它将经历一个加载状态,然后最终发出正确的值。因此,即使您已登录,上述内容也只会重定向到“/signIn”。
  • 修改AppRouterListenable 类以使用enum AuthState { loading, notLoggedIn, loggedIn } 作为状态很容易。这样,重定向回调就拥有了在需要时显示加载路线所需的一切。
【解决方案2】:

你可以使用重定向来做到这一点,但是我想出了一种使用navigatorBuilder 的方法。通过这种方式,您可以保持原始导航器状态(您将被重定向回您最初在 Web 上或通过深度链接访问的任何页面),并且不必不断地重建整个路由器。

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

navigatorBuilder 基本上允许您在 MaterialApp 和 Navigator 之间注入一些小部件。我们使用 Riverpod 的消费者小部件来访问 ref,然后整个路由器不必重建,我们可以使用 ref 访问 auth 状态。

在我的示例中,ref.watch(authControllerProvider) 返回一个 AsyncValue&lt;AuthUser?&gt;,因此如果用户已登录,我们将返回 child(当前导航路线),如果他们已注销,则向他们显示登录屏幕,如果他们正在加载我们可以显示加载屏幕等。

如果您想根据角色重定向用户(例如,只有管理员可以看到管理仪表板),那么该逻辑应该使用 @bizz84 描述的可监听进入重定向功能。

【讨论】:

    猜你喜欢
    • 2021-11-25
    • 2020-11-06
    • 1970-01-01
    • 2017-01-07
    • 1970-01-01
    • 2012-09-27
    • 2015-10-06
    • 2013-07-12
    • 1970-01-01
    相关资源
    最近更新 更多