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

如何基于屏幕尺寸在go_router中条件化嵌套路由?

基于屏幕尺寸动态切换go_router导航方式的实现方案

核心思路

通过动态选择导航器实例(根导航器/Shell导航器)结合屏幕尺寸判断,实现帖子详情页在小屏全屏覆盖导航栏、大屏保留侧边栏的响应式效果,同时满足类型安全路由和尺寸变化无缝切换的要求。

步骤1:定义屏幕尺寸断点

封装工具方法,根据当前上下文判断屏幕类型:

enum ScreenSize { compact, medium, expanded }

ScreenSize getScreenSize(BuildContext context) {
  final width = MediaQuery.sizeOf(context).width;
  if (width < 600) return ScreenSize.compact;
  if (width < 1200) return ScreenSize.medium;
  return ScreenSize.expanded;
}

步骤2:配置双导航器的GoRouter

创建根导航器和Shell导航器的全局Key,通过ShellRoute包裹顶级路由,在builder中根据屏幕尺寸渲染对应的导航UI:

final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorKey = GlobalKey<NavigatorState>();

final router = GoRouter(
  navigatorKey: _rootNavigatorKey,
  routes: [
    ShellRoute(
      navigatorKey: _shellNavigatorKey,
      builder: (context, state, child) {
        final screenSize = getScreenSize(context);
        return Scaffold(
          body: Row(
            children: [
              // 中等/扩展屏幕显示导航Rail
              if (screenSize != ScreenSize.compact)
                NavigationRail(
                  destinations: const [
                    NavigationRailDestination(icon: Icon(Icons.home), label: Text('首页')),
                    NavigationRailDestination(icon: Icon(Icons.list), label: Text('帖子')),
                    NavigationRailDestination(icon: Icon(Icons.person), label: Text('我的')),
                  ],
                  selectedIndex: _getSelectedTabIndex(state.location),
                  onDestinationSelected: (index) => _navigateToTab(index, context),
                ),
              Expanded(child: child),
            ],
          ),
          // 紧凑屏幕显示底部导航栏
          bottomNavigationBar: screenSize == ScreenSize.compact
              ? BottomNavigationBar(
                  items: const [
                    BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
                    BottomNavigationBarItem(icon: Icon(Icons.list), label: '帖子'),
                    BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
                  ],
                  currentIndex: _getSelectedTabIndex(state.location),
                  onTap: (index) => _navigateToTab(index, context),
                )
              : null,
        );
      },
      // 顶级路由配置
      routes: [
        GoRoute(
          path: '/home',
          builder: (context, state) => const HomeScreen(),
        ),
        GoRoute(
          path: '/posts',
          builder: (context, state) => const PostsScreen(),
          // 帖子详情子路由
          routes: [
            GoRoute(
              path: ':id',
              builder: (context, state) => PostDetailScreen(id: state.pathParameters['id']!),
            ),
          ],
        ),
        GoRoute(
          path: '/profile',
          builder: (context, state) => const ProfileScreen(),
        ),
      ],
    ),
  ],
);

// 辅助方法:根据路由路径判断选中的导航标签
int _getSelectedTabIndex(String location) {
  if (location.startsWith('/posts')) return 1;
  if (location.startsWith('/profile')) return 2;
  return 0;
}

// 辅助方法:导航到指定标签页
void _navigateToTab(int index, BuildContext context) {
  switch (index) {
    case 0:
      context.go('/home');
      break;
    case 1:
      context.go('/posts');
      break;
    case 2:
      context.go('/profile');
      break;
  }
}

步骤3:动态选择导航器跳转帖子详情

在帖子列表页(PostsScreen)中,点击帖子时根据当前屏幕尺寸选择使用根导航器或Shell导航器:

class PostsScreen extends StatelessWidget {
  const PostsScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 10,
      itemBuilder: (context, index) {
        final postId = '$index';
        return ListTile(
          title: Text('帖子 $postId'),
          onTap: () => _openPostDetail(postId, context),
        );
      },
    );
  }

  void _openPostDetail(String postId, BuildContext context) {
    final screenSize = getScreenSize(context);
    if (screenSize == ScreenSize.compact) {
      // 紧凑屏幕:用根导航器跳转,全屏覆盖底部导航
      _rootNavigatorKey.currentState?.go('/posts/$postId');
    } else {
      // 大屏:用当前Shell导航器跳转,保留侧边栏
      context.go('/posts/$postId');
    }
  }
}

步骤4:实现类型安全路由

使用go_router_builder生成类型安全的路由,避免硬编码路径:

  1. 添加依赖:
dependencies:
  go_router_builder: ^2.3.0
  1. 定义路由类:
part 'router.g.dart';

@TypedGoRoute<HomeRoute>(path: '/home')
class HomeRoute extends GoRouteData {
  const HomeRoute();
  @override
  Widget build(BuildContext context, GoRouterState state) => const HomeScreen();
}

@TypedGoRoute<PostsRoute>(path: '/posts')
class PostsRoute extends GoRouteData {
  const PostsRoute();
  @override
  Widget build(BuildContext context, GoRouterState state) => const PostsScreen();
}

@TypedGoRoute<PostDetailRoute>(path: '/posts/:id')
class PostDetailRoute extends GoRouteData {
  const PostDetailRoute({required this.id});
  final String id;
  @override
  Widget build(BuildContext context, GoRouterState state) => PostDetailScreen(id: id);
}

@TypedGoRoute<ProfileRoute>(path: '/profile')
class ProfileRoute extends GoRouteData {
  const ProfileRoute();
  @override
  Widget build(BuildContext context, GoRouterState state) => const ProfileScreen();
}
  1. 运行代码生成命令:
flutter pub run build_runner build
  1. 修改导航代码为类型安全方式:
// 导航到首页
const HomeRoute().go(context);

// 打开帖子详情
void _openPostDetail(String postId, BuildContext context) {
  final screenSize = getScreenSize(context);
  final route = PostDetailRoute(id: postId);
  if (screenSize == ScreenSize.compact) {
    _rootNavigatorKey.currentState?.push(route.location);
  } else {
    route.go(context);
  }
}

步骤5:处理屏幕尺寸变化的无缝切换

由于ShellRoute的builder会监听MediaQuery变化,当屏幕尺寸调整时,导航UI会自动切换为底部导航/导航Rail(可自行扩展抽屉逻辑)。同时,帖子详情页的导航器归属会在下次跳转时自动适配新的屏幕尺寸,实现无缝切换。

内容的提问来源于stack exchange,提问作者Bryson Thill

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 15:42:05