如何定义一个依赖于Provider的GoRouter?

7

我正在我的Flutter应用中集成GoRouter,而我已经在使用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,
    );
  }

这样做正确吗?


看起来没问题,因为我在运行时获取了一个路由并导航到它,这样应该也可以。(我不是完全确定,因为我只改变了我的路由一次)。如果这不会给你编译时错误,那么如果你不确定的话,你应该在运行时进行广泛的测试 :) - Ruchit
2个回答

2
我认为你不应该拨打这条线路。
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: [],
  );
}

请注意,这段代码在身份验证状态更改时不是响应式的,也就是说它不会刷新路由。因此,在进行登录/注销操作时,需要执行显式导航事件。或者,您可以使用refreshListenable参数。

我不认为这种方法适用于 Firebase 认证之类的东西。因为路由器是被动的,而不是监听的,所以第一次调用类似 appRouterListenable.isLoggedIn 的函数,在身份验证系统确定您是否已经登录期间将返回 false - 它会经过加载状态,然后最终发出正确的值。因此,即使您已登录,以上代码也只会重定向到 "/signIn"。 - Jon Mountjoy
很容易修改AppRouterListenable类以使用enum AuthState { loading, notLoggedIn, loggedIn }作为状态。这样,重定向回调就有了必要的一切来显示需要的加载路由。 - bizz84
1
未来读者注意:refreshListenable可能不再按预期工作:github.com/flutter/flutter/issues/116651。 - elllot

2
您可以使用重定向的方式来实现,但我想到了一种使用navigatorBuilder的方法。这样您就可以保持原始导航器状态(您将被重定向回原始访问的网页或深度链接页面),整个路由器不必不断地重建。
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的consumer widget来访问ref,然后整个路由器就不必重建,我们可以使用ref访问auth状态。

在我的示例中,ref.watch(authControllerProvider)返回一个AsyncValue<AuthUser?>,因此如果用户已登录,则返回child(当前导航路由),如果他们未登录,则显示登录屏幕,如果他们正在加载,则可以显示加载屏幕等。

如果您想基于角色重定向用户(例如,只有管理员才能看到管理员仪表板),则该逻辑应该使用可监听对象作为@bizz84所描述的重定向函数中。


1
在 Go Router 5.0.0 中,他们移除了 navigatorBuilder,我喜欢你的注入方式,这种方法有更新吗? - Sayyid J
1
@SayyidJ 这里是 Go 路由器 5 的迁移指南,其中提到你可以使用 Material 应用程序构建器代替 :) - HJo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接