You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于go_router与flutter_bloc实现认证/未认证流程的响应式路由跳转

Flutter 基于认证状态的GoRouter导航问题与解决方案

需求说明

  • 应用启动时根据认证状态跳转:已认证→HomeScreen,未认证→LoginScreen,状态未知→SplashScreen
  • 已认证状态下,支持在HomeScreen、TestScreen1、TestScreen2之间自由导航
  • 任何时刻认证状态变化时,自动重定向到对应页面

当前遇到的问题

问题1:认证流程内导航报错

从HomeScreen跳转TestScreen1/TestScreen2时,触发断言错误:

Tried to listen to a value exposed with provider, from outside of the widget tree.

This is likely caused by an event handler (like a button's onPressed) that called
Provider.of without passing listen: false.

To fix, write:
Provider.of<$T>(context, listen: false);

It is unsupported because may pointlessly rebuild the widget associated to the
event handler, when the widget tree doesn't care about the value.

The context used was: $context

问题2:Web端手动修改URL报错

在HomeScreen(路径/auth/home_screen)时手动修改URL为/login_screen,抛出异常:

========= Exception caught by foundation library ====================================================
The following assertion was thrown while dispatching notifications for GoRouteInformationProvider:
Tried to listen to a value exposed with provider, from outside of the widget tree.

This is likely caused by an event handler (like a button's onPressed) that called
Provider.of without passing listen: false.

注:不想用context.read替换context.watch,因为需要监听Bloc状态变化

现有实现代码

AppRouter代码

class AppRouter {
  GoRouter get router => _goRouter;

  late final GoRouter _goRouter = GoRouter(
    routes: <GoRoute>[
      GoRoute(
        path: '/',
        name: 'SPLASH',
        builder: (context, state) => const SplashScreen(),
      ),
      GoRoute(
        path: '/auth',
        name: 'AUTH',
        builder: (context, state) => HomeScreen(),
        routes: <GoRoute>[
          GoRoute(
            path: 'home_screen',
            name: 'HOME',
            builder: (context, state) => HomeScreen(),
          ),
          GoRoute(
            path: 'test_screen_1',
            name: 'TEST_SCREEN_1',
            builder: (context, state) => TESTSCREEN1(),
          ),
          GoRoute(
            path: 'test_screen_2',
            name: 'TEST_SCREEN_2',
            builder: (context, state) => TESTSCREEN2(),
          ),
        ],
      ),
      GoRoute(
        path: '/login_screen',
        name: 'LOGIN',
        builder: (context, state) => LoginScreen(),
      ),
    ],
    redirect: (context, state) {
      final authState = context.watch<AuthBloc>().state;
      final loginLocation = state.namedLocation('LOGIN');
      final authFlow = state.namedLocation('AUTH');
      final splashLocation = state.namedLocation('SPLASH');

      if (authState is AuthStateAuthenticated) {
        return authFlow;
      } else if (authState is AuthStateUnAuthenticated) {
        return loginLocation;
      } else {
        return splashLocation;
      }
    },
  );
}

App代码

class App extends StatelessWidget {

  AppRouter appRouter = AppRouter();
  @override
  Widget build(BuildContext context) {
    return RepositoryProvider<AuthService>(
      create: (context) => AuthService(),
      child: MultiBlocProvider(
        providers: [
          BlocProvider<AuthBloc>(create: (context) => AuthBloc(authService: RepositoryProvider.of<AuthService>(context))),
        ],
        child: MaterialApp.router(
          routerDelegate: appRouter.router.routerDelegate,
          routeInformationParser: appRouter.router.routeInformationParser,
          routeInformationProvider: appRouter.router.routeInformationProvider,
        ),
      ),
    );
  }
}

解决方案

核心问题分析

GoRouter的redirect方法不在Widget树上下文内执行,直接用context.watch会触发"outside of the widget tree"错误。需要通过Bloc的状态流监听变化,而非依赖Widget树上下文。

步骤1:重构AppRouter,绑定Bloc流作为刷新源

class AppRouter {
  final AuthBloc authBloc;

  AppRouter(this.authBloc);

  late final GoRouter _goRouter = GoRouter(
    routes: <GoRoute>[
      GoRoute(
        path: '/',
        name: 'SPLASH',
        builder: (context, state) => const SplashScreen(),
      ),
      GoRoute(
        path: '/auth',
        name: 'AUTH',
        builder: (context, state) => HomeScreen(),
        routes: <GoRoute>[
          GoRoute(
            path: 'home_screen',
            name: 'HOME',
            builder: (context, state) => HomeScreen(),
          ),
          GoRoute(
            path: 'test_screen_1',
            name: 'TEST_SCREEN_1',
            builder: (context, state) => TESTSCREEN1(),
          ),
          GoRoute(
            path: 'test_screen_2',
            name: 'TEST_SCREEN_2',
            builder: (context, state) => TESTSCREEN2(),
          ),
        ],
      ),
      GoRoute(
        path: '/login_screen',
        name: 'LOGIN',
        builder: (context, state) => LoginScreen(),
      ),
    ],
    redirect: (context, state) {
      // 用read获取当前状态,无需监听(状态变化由refreshListenable触发)
      final authState = context.read<AuthBloc>().state;
      final loginLocation = state.namedLocation('LOGIN');
      final authHomeLocation = state.namedLocation('HOME');
      final splashLocation = state.namedLocation('SPLASH');

      // 已认证状态:不在认证流程内则跳Home,否则留当前页
      if (authState is AuthStateAuthenticated) {
        return state.fullPath!.startsWith('/auth') ? null : authHomeLocation;
      } 
      // 未认证状态:不在登录页则跳转,否则留当前页
      else if (authState is AuthStateUnAuthenticated) {
        return state.fullPath == '/login_screen' ? null : loginLocation;
      } 
      // 状态未知:不在闪屏页则跳转,否则留当前页
      else {
        return state.fullPath == '/' ? null : splashLocation;
      }
    },
    // 绑定Bloc流,状态变化时自动触发重定向
    refreshListenable: GoRouterRefreshStream(authBloc.stream),
  );

  GoRouter get router => _goRouter;
}

// 自定义Listenable,将Bloc流转换为GoRouter可监听的源
class GoRouterRefreshStream extends ChangeNotifier {
  GoRouterRefreshStream(Stream<dynamic> stream) {
    _subscription = stream.listen((_) => notifyListeners());
  }

  late final StreamSubscription<dynamic> _subscription;

  @override
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }
}

步骤2:修改App类,传递AuthBloc实例

在BlocProvider创建后再初始化AppRouter,确保能获取到Bloc实例:

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RepositoryProvider<AuthService>(
      create: (context) => AuthService(),
      child: BlocProvider<AuthBloc>(
        create: (context) => AuthBloc(authService: RepositoryProvider.of<AuthService>(context)),
        child: Builder(
          builder: (context) {
            final authBloc = BlocProvider.of<AuthBloc>(context);
            final appRouter = AppRouter(authBloc);
            return MaterialApp.router(
              routerDelegate: appRouter.router.routerDelegate,
              routeInformationParser: appRouter.router.routeInformationParser,
              routeInformationProvider: appRouter.router.routeInformationProvider,
            );
          },
        ),
      ),
    );
  }
}

步骤3:认证流程内导航修复

现在在HomeScreen等页面内直接使用以下代码导航即可,不会再触发错误:

GoRouter.of(context).pushNamed('TEST_SCREEN_1');

关键说明

  • 通过GoRouterRefreshStream将AuthBloc的状态流转换为GoRouter的刷新源,状态变化时自动触发重定向
  • redirect内用context.read获取当前状态,避免在非Widget树上下文内监听状态
  • 重定向逻辑增加当前路径判断,避免无限重定向循环

内容的提问来源于stack exchange,提问作者Taha Aslam

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 00:54:58