如何定义依赖于Provider的GoRouter?

发布于 2025-01-10 16:29:38 字数 1653 浏览 0 评论 0原文

我正在将 GoRouter 集成到我的 Flutter 应用程序中,我已经在使用 Riverpod。我有一个 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,
    );
  }

这是正确的方法吗?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

浅浅淡淡 2025-01-17 16:29:38

我不认为您应该

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

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.

面犯桃花 2025-01-17 16:29:38

您可以使用重定向来完成此操作,但是我想出了一种使用 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 的消费者小部件来访问 ref,然后不必重建整个路由器,并且我们可以使用 ref 访问身份验证状态。

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

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

You can do it this way using redirect, however I've come up with a way that uses navigatorBuilder. This way you maintain the original navigator state (you will be redirected back to whichever page you originally visited on web or with deep linking), and the whole router doesn't have to be constantly be rebuilt.

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 basically allows you to inject some widget between the MaterialApp and the Navigator. We use Riverpod's consumer widget to access the ref and then the whole router doesn't have to be rebuilt, and we can access auth state using the ref.

In my example, ref.watch(authControllerProvider) returns an AsyncValue<AuthUser?>, so if the user is logged in, we return the child (current navigated route), if they are logged out, show them login screen, and if they are loading we can show a loading screen etc.

If you want to redirect users based on roles (e.g. only admin can see admin dashboard), then that logic should go into the redirect function using a listenable as @bizz84 described.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文