如何基于屏幕尺寸在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生成类型安全的路由,避免硬编码路径:
- 添加依赖:
dependencies: go_router_builder: ^2.3.0
- 定义路由类:
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(); }
- 运行代码生成命令:
flutter pub run build_runner build
- 修改导航代码为类型安全方式:
// 导航到首页 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
相关产品推荐
相关产品推荐

