go_router
是一个用于 Flutter 应用的第三方路由管理库,它简化了应用内的路由导航逻辑,提供了声明式的路由配置方式,同时对 URL 有很好的支持,在 Web、移动端和桌面端都能表现出色。开始了解以前,你可以先看一下原生路由导航:Flutter 路由与导航
go_router特性
- GoRouter的配置(routes, redirect, errorBuilder)
- 导航方法(go, push, pop)
- 命名路由(goNamed, pushNamed)
- 路由参数传递(queryParams, extra)
- 路由守卫(redirect函数)
- 错误处理(errorBuilder)
- 路由状态获取(location, route名称)
- 嵌套路由(ShellRoute)
- 监听路由变化
1、 路由配置
GoRouter
是 go_router
库的核心类,用于配置路由信息。
GoRouter 构造函数
swift
GoRouter({
required List<RouteBase> routes,
GoRouterRedirect? redirect,
List<NavigatorObserver>? observers,
GlobalKey<NavigatorState>? navigatorKey,
String? initialLocation,
bool? debugLogDiagnostics,
RouteInformationParser<Uri>? routeInformationParser,
RouterDelegate<Uri>? routerDelegate,
BackButtonDispatcher? backButtonDispatcher,
String restorationScopeId,
})
routes
:必填参数,用于定义应用的路由列表。redirect
:可选参数,用于在导航时进行重定向。observers
:可选参数,用于监听导航事件的观察者列表。navigatorKey
:可选参数,用于访问底层的Navigator
实例。initialLocation
:可选参数,指定应用启动时的初始路由。debugLogDiagnostics
:可选参数,是否在调试模式下打印路由诊断信息。
使用示例
php
final GoRouter _router = GoRouter(
routes: <RouteBase>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) {
return const HomePage();
},
),
GoRoute(
path: '/details',
builder: (BuildContext context, GoRouterState state) {
return const DetailsPage();
},
),
],
);
GoRoute 路由定义
dart
GoRoute({
required String path,
required WidgetBuilder builder,
List<RouteBase>? routes,
GoRouterRedirect? redirect,
LocalKey? key,
String? name,
PageBuilder? pageBuilder,
bool maintainState = true,
bool fullscreenDialog = false,
})
-
required String path
:这是一个必填参数,用于指定该路由对应的路径。路径可以是固定的字符串,也可以包含路径参数。路径参数使用冒号:
开头,用于匹配动态的值。 -
required WidgetBuilder builder
:同样是必填参数,它是一个函数,用于构建该路由对应的Widget
。该函数接收两个参数:BuildContext context
和GoRouterState state
,并返回一个Widget
。csharpGoRoute( path: '/', builder: (BuildContext context, GoRouterState state) { return const HomePage(); }, )
-
List<RouteBase>? routes
:可选参数,用于定义该路由的子路由列表。子路由可以进一步细分当前路由的导航结构。lessGoRoute( path: '/settings', builder: (context, state) => SettingsPage(), routes: [ GoRoute( path: 'notifications', builder: (context, state) => NotificationSettingsPage(), ), ], )
/settings
是父路由,/settings/notifications
是它的子路由。
-
GoRouterRedirect? redirect
:是一个重定向函数。该函数接收BuildContext context
和GoRouterState state
作为参数,并返回一个字符串或null
。如果返回一个字符串,则表示重定向到该路径;如果返回null
,则正常导航到当前路由。javascriptGoRoute( path: '/secret', redirect: (context, state) { // 假设 isUserAuthenticated 是一个检查用户是否认证的函数 if (!isUserAuthenticated()) { return '/login'; } return null; }, builder: (context, state) => SecretPage(), )
如果用户未认证,访问
/secret
路径时会重定向到/login
路径。 -
LocalKey? key
:于给该路由的Widget
提供一个唯一的键。键可以帮助 Flutter 更准确地识别和更新Widget
。 -
String? name
:为该路由指定一个名称。通过名称可以更方便地进行导航,而不需要记住具体的路径。 -
pageBuilder
:是一个用于构建Page
对象的函数。与builder
不同,pageBuilder
可以自定义页面的过渡效果等。如果同时提供了builder
和pageBuilder
,pageBuilder
会优先使用。lessGoRoute( path: '/details', pageBuilder: (context, state) { return MaterialPage( key: state.pageKey, child: DetailsPage(), ); }, )
-
maintainState = true
:表示当该路由离开导航栈时,是否保留其状态。如果设置为true
,当再次返回该路由时,会恢复之前的状态;如果设置为false
,每次进入该路由都会重新创建Widget
。 -
fullscreenDialog = false
: 表示该路由是否以全屏对话框的形式显示。如果设置为true
,在 Android 上会以全屏对话框的样式显示,在 iOS 上会有不同的过渡效果。
路由参数
GoRouter
的每一个路由都通过 GoRoute
对象来配置,我们可以在构建 GoRoute
对象时来配置路由参数。路由参数典型的就是路径参数,比如 /path/:{路径参数}
,这个时候 GoRoute
的路径参数和很多 Web 框架的路由是一样的,通过一个英文冒号加参数名称就可以配置,之后我们可以在回调方法中通过 GoRouterState
对象获取路径参数,这个参数就可以传递到路由跳转目的页面。
1、路径参数(Path Parameters)
路径参数用于在路由路径中传递动态值,例如用户 ID、文章 ID 等。在定义路由时,使用冒号 :
来标记路径参数。
dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// 定义一个接收路径参数的页面
class UserPage extends StatelessWidget {
final String userId;
const UserPage({Key? key, required this.userId}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('User $userId')),
body: Center(
child: Text('User ID: $userId'),
),
);
}
}
// 配置 GoRouter,定义包含路径参数的路由
final GoRouter _router = GoRouter(
routes: <RouteBase>[
GoRoute(
path: '/users/:id',
builder: (BuildContext context, GoRouterState state) {
// 从 GoRouterState 中获取路径参数
final String userId = state.params['id']!;
return UserPage(userId: userId);
},
),
],
);
void main() {
runApp(
MaterialApp.router(
routerConfig: _router,
),
);
}
导航到包含路径参数的路由
go
// 导航到包含路径参数的路由
GoRouter.of(context).go('/users/123');
2、查询参数(Query Parameters)
查询参数用于在 URL 中传递额外的信息,通常用于过滤、排序等操作。查询参数以问号 ?
开头,多个参数之间用 &
分隔。
php
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// 定义一个接收查询参数的页面
class SearchPage extends StatelessWidget {
final String? query;
const SearchPage({Key? key, this.query}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Search')),
body: Center(
child: Text('Search query: ${query ?? 'No query'}'),
),
);
}
}
// 配置 GoRouter,定义接收查询参数的路由
final GoRouter _router = GoRouter(
routes: <RouteBase>[
GoRoute(
path: '/search',
builder: (BuildContext context, GoRouterState state) {
// 从 GoRouterState 中获取查询参数
final String? query = state.queryParams['q'];
return SearchPage(query: query);
},
),
],
);
void main() {
runApp(
MaterialApp.router(
routerConfig: _router,
),
);
}
导航到包含查询参数的路由
go
// 导航到包含查询参数的路由
GoRouter.of(context).go('/search?q=flutter');
3、命名路由与参数传递
php
// 定义带名称的路由,包含路径参数
final GoRouter _router = GoRouter(
routes: <RouteBase>[
GoRoute(
name: 'user',
path: '/users/:id',
builder: (BuildContext context, GoRouterState state) {
final String userId = state.params['id']!;
return UserPage(userId: userId);
},
),
],
);
// 通过名称导航并传递路径参数
GoRouter.of(context).goNamed('user', params: {'id': '456'});
// 通过名称导航并传递查询参数
GoRouter.of(context).goNamed('search', queryParams: {'q': 'dart'});
2、 导航方法
基本配置
context.go
直接导航到指定的路由,会替换当前的路由栈,即当前页面会被新页面替换。
go
context.go('/details');
context.push
将新的路由页面推送到路由栈中,当前页面不会被替换,用户可以通过返回操作回到上一个页面。
arduino
context.push('/details');
context.pop
从路由栈中弹出当前页面,返回到上一个页面。
ini
context.pop();
context.goNamed
和 context.pushNamed
当你为路由配置了名称时,可以使用这两个方法通过名称进行导航。
php
final GoRouter _router = GoRouter(
routes: <RouteBase>[
GoRoute(
name: 'home',
path: '/',
builder: (BuildContext context, GoRouterState state) {
return const HomePage();
},
routes: <RouteBase>[
GoRoute(
name: 'details',
path: 'details',
builder: (BuildContext context, GoRouterState state) {
return const DetailsPage();
},
),
],
),
],
);
// 使用名称导航
context.goNamed('details');
context.pushNamed('details');
嵌套导航 ShellRoute
在 go_router
中,嵌套导航允许你在应用的某个部分实现独立的路由管理,例如在底部导航栏或者侧边栏的不同标签页内进行各自的路由切换。以下是关于 go_router
嵌套导航的详细介绍和示例代码。
less
ShellRoute(
builder: (context, state, child) => Scaffold(
body: child,
bottomNavigationBar: BottomNavBar(),
),
routes: [
GoRoute(path: '/books', ...),
GoRoute(path: '/movies', ...),
],
)
实现思路
- 外层路由配置:定义整个应用的主要路由结构,包含嵌套导航的父路由。
- 内层路由配置:在父路由内部定义子路由,用于管理嵌套导航的页面。
- 使用
ShellRoute
:ShellRoute
是go_router
中用于实现嵌套导航的关键组件,它可以包裹子路由,提供一个共享的布局。
less
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// 定义页面
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
context.go('/books');
},
child: const Text('Go to Books'),
),
ElevatedButton(
onPressed: () {
context.go('/movies');
},
child: const Text('Go to Movies'),
),
],
),
),
);
}
}
class BooksPage extends StatelessWidget {
const BooksPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Books')),
body: Center(
child: ElevatedButton(
onPressed: () {
context.go('/books/details');
},
child: const Text('Go to Book Details'),
),
),
);
}
}
class BookDetailsPage extends StatelessWidget {
const BookDetailsPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Book Details')),
body: const Center(child: Text('This is the book details page.')),
);
}
}
class MoviesPage extends StatelessWidget {
const MoviesPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Movies')),
body: const Center(child: Text('This is the movies page.')),
);
}
}
// 配置路由
final GoRouter _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
),
ShellRoute(
builder: (context, state, child) {
return Scaffold(
body: child,
);
},
routes: [
GoRoute(
path: '/books',
builder: (context, state) => const BooksPage(),
routes: [
GoRoute(
path: 'details',
builder: (context, state) => const BookDetailsPage(),
),
],
),
GoRoute(
path: '/movies',
builder: (context, state) => const MoviesPage(),
),
],
),
],
);
void main() {
runApp(
MaterialApp.router(
routerConfig: _router,
),
);
}
-
1、外层路由
- 定义了根路由
/
,对应HomePage
,这是应用的起始页面。
- 定义了根路由
-
2、
ShellRoute
-
ShellRoute
包裹了两个子路由/books
和/movies
,它的builder
方法返回一个Scaffold
,用于提供一个共享的布局。 -
子路由的页面会显示在
Scaffold
的body
中。
-
-
内层路由
/books
路由下还有一个子路由/books/details
,对应BookDetailsPage
,实现了在BooksPage
内部的嵌套导航。
3、 路由守卫与拦截
php
// 路由重定向函数
String? redirectLogic(GoRouterState state) {
final bool isGoingToLogin = state.matchedLocation == '/login';
// 如果用户未登录且不是前往登录页,则重定向到登录页
if (!isUserLoggedIn && !isGoingToLogin) {
return '/login';
}
// 如果用户已登录且正在前往登录页,则重定向到仪表盘页
if (isUserLoggedIn && isGoingToLogin) {
return '/dashboard';
}
return null; // 允许导航到目标路由
}
// 配置路由
final GoRouter _router = GoRouter(
redirect: redirectLogic,
routes: [
GoRoute(
path: '/login',
builder: (context, state) => const LoginPage(),
),
GoRoute(
path: '/dashboard',
builder: (context, state) => const DashboardPage(),
),
],
);
- 模拟用户登录状态 :使用
isUserLoggedIn
布尔变量来模拟用户的登录状态。 redirectLogic
函数- 该函数接收
GoRouterState
对象,该对象包含了当前导航的相关信息,如目标路由的路径。 - 当用户未登录且尝试访问除登录页之外的页面时,函数返回
/login
,将用户重定向到登录页。 - 当用户已登录且尝试访问登录页时,函数返回
/dashboard
,将用户重定向到仪表盘页。 - 如果不需要重定向,函数返回
null
,允许用户正常导航到目标路由。
- 该函数接收
GoRouter
配置 :在GoRouter
的构造函数中传入redirect
参数,将其设置为redirectLogic
函数,这样每次导航前都会执行该函数的逻辑。
4、 错误处理
less
final GoRouter _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
),
],
errorBuilder: (context, state) {
return ErrorPage(error: state.error);
},
);
errorBuilder
参数指定了一个函数,当导航出错时会调用这个函数。该函数接收context
和state
两个参数,state.error
包含了具体的错误信息,我们将其传递给ErrorPage
来显示。
上述示例主要处理了导航到不存在路由的情况,也就是 404 错误。当用户尝试访问未在 routes
中定义的路由时,go_router
会触发 errorBuilder
来显示错误页面。
除了 404 错误,在实际开发中还可能遇到其他类型的错误,比如在路由的 builder
函数中抛出异常。这些错误同样会触发 errorBuilder
,你可以根据 state.error
的具体类型进行不同的处理,例如:
vbnet
errorBuilder: (context, state) {
if (state.error is SomeSpecificException) {
// 处理特定类型的异常
return SpecificErrorPage(error: state.error);
}
return ErrorPage(error: state.error);
}
5、路由状态获取
1、在路由构建器中获取状态
在 GoRoute
的 builder
或 pageBuilder
函数中,会传入一个 GoRouterState
对象,你可以通过它来获取路由的相关信息。
ini
GoRoute(
path: 'details/:id',
builder: (BuildContext context, GoRouterState state) {
// 获取路径参数
final String id = state.pathParameters['id']!;
// 获取查询参数
final String? queryParam = state.queryParameters['param'];
// 获取完整的 URI
final Uri uri = state.uri;
return DetailsPage(id: id);
},
);
2 在 Widget 中获取当前路由状态
scala
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
final GoRouterState state = GoRouterState.of(context);
final String? currentPath = state.matchedLocation;
return Text('Current path: $currentPath');
}
}
7、监听路由变化
1、使用 GoRouter
的 refreshListenable
GoRouter
提供了 refreshListenable
选项,你可以传入一个 Listenable
对象,当路由发生变化时,GoRouter
会通知这个 Listenable
,进而触发更新。
less
/ 创建一个 ValueNotifier 作为可监听对象
final ValueNotifier<String> routeNotifier = ValueNotifier<String>('');
// 定义路由
final GoRouter _router = GoRouter(
refreshListenable: routeNotifier,
routes: <GoRoute>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) {
// 更新路由信息
routeNotifier.value = state.matchedLocation;
return const HomePage();
},
routes: <GoRoute>[
GoRoute(
path: 'details/:id',
builder: (BuildContext context, GoRouterState state) {
// 更新路由信息
routeNotifier.value = state.matchedLocation;
final String id = state.pathParameters['id']!;
return DetailsPage(id: id);
},
),
],
),
],
);
// 监听路由变化的 Widget
class RouteListenerWidget extends StatelessWidget {
const RouteListenerWidget({super.key});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<String>(
valueListenable: routeNotifier,
builder: (BuildContext context, String value, Widget? child) {
return Text('Current Route: $value');
},
);
}
}
void main() {
runApp(
MaterialApp.router(
routerConfig: _router,
home: Column(
children: [
const RouteListenerWidget(),
Expanded(
child: Router(
routerConfig: _router,
),
),
],
),
),
);
}
- 首先创建了一个
ValueNotifier<String>
类型的routeNotifier
作为可监听对象,并将其传递给GoRouter
的refreshListenable
属性。 - 在每个路由的
builder
函数中,更新routeNotifier
的值为当前匹配的路径。 - 创建了一个
RouteListenerWidget
,使用ValueListenableBuilder
监听routeNotifier
的变化,并在路由变化时更新显示的路由信息。
2、使用 GoRouter
的 observers
dart
class RouterObserver extends NavigatorObserver {
void log(value) => debugPrint('MyNavObserver:$value');
/// 当一个新的路由被推送到导航栈时,此方法会被调用。
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
log('新的路由被推送到导航栈: ${route.toString()}, previousRoute= ${previousRoute?.toString()}');
}
/// 当一个路由从导航栈中弹出时,此方法会被调用。route 参数表示被弹出的路由,previousRoute 参数
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
log('路由从导航栈中弹出: ${route.toString()}, previousRoute= ${previousRoute?.toString()}');
}
/// 当一个路由从导航栈中被移除时,此方法会被调用。移除路由和弹出路由不同,移除操作可以移除导航栈中任意位置的路由,而弹出操作只能移除栈顶的路由。
/// route 参数表示被移除的路由,previousRoute 参数表示在该路由移除后,其下一个路由(如果存在的话)。
@override
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
log('didRemove: ${route.toString()}, previousRoute= ${previousRoute?.toString()}');
}
@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
log('didReplace: new= ${newRoute?.toString()}, old= ${oldRoute?.toString()}');
}
/// 当用户开始进行一个导航手势(如在 iOS 上从屏幕边缘向左滑动返回上一页)时,此方法会被调用。
/// route 参数表示当前正在操作的路由,previousRoute 参数表示在手势操作后可能会显示的前一个路由(如果存在的话)。
@override
void didStartUserGesture(
Route<dynamic> route, Route<dynamic>? previousRoute) {
log('didStartUserGesture: ${route.toString()}, '
'previousRoute= ${previousRoute?.toString()}');
}
/// 用户结束导航手势时,此方法会被调用。无论手势是否成功完成导航操作,只要手势结束,就会触发这个方法。
@override
void didStopUserGesture() {
log('didStopUserGesture');
}
}
8. 其他实用 API
- 刷新路由 :
router.refresh()
(常用于登录状态变化后强制重定向)。 - 获取当前路径 :
final location = router.location();
- 获取路由名称 :
final routeName = router.routeInformationProvider.value.name;
路由封装
go_router是一个声明式的路由库,支持深度链接和导航,适合复杂的路由场景。用户可能已经了解了基础的路由配置,但现在需要将路由配置进行封装,以提高代码的可维护性和扩展性。
- 路由配置集中管理:将所有路由路径、名称定义在一个单独的类或文件中,避免在代码中散落字符串,减少错误。
- 模块化路由配置:将不同功能模块的路由分别封装到不同的文件中,便于团队协作和模块化开发。
- 路由守卫封装:统一处理路由跳转前的权限验证或条件检查,例如登录状态检查。
- 路由跳转方法封装:提供统一的跳转方法,简化上下文的使用,特别是无需context的情况。
- 自定义路由过渡动画:封装路由跳转的动画效果,保持应用内的一致性。
1. 路由路径集中管理
将路由路径和名称统一管理,避免硬编码:
ini
// lib/routes/app_routes.dart
abstract class AppRoutes {
static const home = '/';
static const details = '/details';
static const profile = '/profile';
static const settings = '/settings';
}
2. 模块化路由配置
将不同模块的路由定义拆分到独立文件中:
dart
// lib/routes/home_route.dart
import 'package:go_router/go_router.dart';
import '../pages/home_page.dart';
GoRoute get homeRoute => GoRoute(
path: AppRoutes.home,
pageBuilder: (context, state) => MaterialPage(
key: state.pageKey,
child: const HomePage(),
),
);
// lib/routes/details_route.dart
import 'package:go_router/go_router.dart';
import '../pages/details_page.dart';
GoRoute get detailsRoute => GoRoute(
path: AppRoutes.details,
pageBuilder: (context, state) => MaterialPage(
key: state.pageKey,
child: const DetailsPage(),
),
);
3. 全局路由配置整合
统一整合所有模块路由:
arduino
// lib/routes/router_config.dart
import 'package:go_router/go_router.dart';
import 'home_route.dart';
import 'details_route.dart';
final appRouter = GoRouter(
initialLocation: AppRoutes.home,
routes: [
homeRoute,
detailsRoute,
// 添加更多路由...
],
);
4. 路由守卫封装
实现全局路由守卫(例如登录验证):
arduino
// lib/routes/route_guard.dart
import 'package:go_router/go_router.dart';
class RouteGuard {
static bool isLoggedIn = false;
static FutureOr<String?> authGuard(
BuildContext context,
GoRouterState state,
) {
final isLoginPage = state.location == AppRoutes.login;
if (!isLoggedIn && !isLoginPage) {
return AppRoutes.login; // 跳转登录页
}
if (isLoggedIn && isLoginPage) {
return AppRoutes.home; // 已登录时禁止返回登录页
}
return null; // 允许导航
}
}
// 在路由配置中启用
final appRouter = GoRouter(
redirect: RouteGuard.authGuard,
// ...其他配置
);
5. 路由跳转工具类
封装无需 context
的跳转方法:
javascript
// lib/utils/navigation_service.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class NavigationService {
static final GlobalKey<NavigatorState> navigatorKey =
GlobalKey<NavigatorState>();
static BuildContext get context =>
navigatorKey.currentState!.context;
static void pushNamed(String routeName, {Object? extra}) {
context.pushNamed(routeName, extra: extra);
}
static void goNamed(String routeName, {Object? extra}) {
context.goNamed(routeName, extra: extra);
}
static void pop() => context.pop();
}
// 在 MaterialApp 中注入
MaterialApp.router(
routerConfig: appRouter,
navigatorKey: NavigationService.navigatorKey,
);
6. 参数传递标准化
定义统一参数传递模型
kotlin
// lib/models/route_args.dart
class DetailsPageArgs {
final String id;
final String? source;
DetailsPageArgs({
required this.id,
this.source,
});
}
// 使用示例
NavigationService.pushNamed(
AppRoutes.details,
extra: DetailsPageArgs(id: '123', source: 'home'),
);
// 在页面中获取参数
final args = state.extra as DetailsPageArgs;
7 错误路由处理
统一404页面处理:
less
final appRouter = GoRouter(
errorPageBuilder: (context, state) => MaterialPage(
child: Scaffold(
body: Center(
child: Text('页面不存在: ${state.location}'),
),
),
),
// ...其他配置
);
8、 完整项目结构示例
bash
lib/
├── main.dart
├── routes/
│ ├── app_routes.dart # 路由路径常量
│ ├── router_config.dart # 路由配置入口
│ ├── home_route.dart # 首页路由配置
│ ├── details_route.dart # 详情页路由配置
│ └── route_guard.dart # 路由守卫
├── models/
│ └── route_args.dart # 路由参数模型
├── utils/
│ └── navigation_service.dart # 导航服务
└── pages/
├── home_page.dart
└── details_page.dart