文章来源
Integrating Bottom Navigation with Go Router in Flutter
前言
本文探讨了如何使用 Flutter 路由库 Go Router 的强大功能,演示了 Go Router 怎样结合 Tab 标签页,实现高级导航, 以及如何使用根导航器,如何保留 Tab 标签页的特定状态。在复杂的 Flutter 应用中, 使用 Go Router,对于实现无缝且高效的导航是非常有好处的。
介绍
在 Flutter 应用开发中,路由管理是构建高效应用程序的关键要素之一。 尽管 Flutter 自身搭载了一套路由导航系统,但 Go Router 等第三方库提供了更强大、更直观的路由管理方案。 本文将详细介绍如何在 Flutter 中利用单例模式的辅助类,将 Go Router 与BottomNavigationBar有效结合,以优化应用导航体验。
什么是路由
路由是一种控制应用不同页面之间跳转的机制,在 Flutter 中,路由是通过Navigator2.0提供的一组 API 实现的, 通过这些 API,我们能够灵活的控制页面之间的跳转和过渡。
为什么选择 Go Router
在开发流程中,选择合适的路由库至关重要,Go Router 以其全面的功能特性脱颖而出,成为一项具有吸引力的选择。 以下列出选择 Go Router 的几个理由:
- 声明式语法:Go Router 使用声明式语法,让你的路由逻辑简单易读,修改方便。 这种直观的语法,简化了路由的定义,让开发者非常容易上手。全部的路由定义都会集中在一起,方便统一查看修改。
::: tip 什么是声明式语法 声明式语法(Declarative Syntax)是一种编程范式,它强调的是要做什么,而不是如何做。 在声明式编程中,开发者只需指定程序的目标或结果,而不需要详细描述实现这一目标的具体步骤。 这种方式使得代码更加简洁、易于理解和维护。
在 Flutter 的 Go Router 中,声明式语法允许开发者定义应用的路由结构,而无需关心页面之间导航的具体逻辑。 这样,开发者可以更专注于应用的结构和用户界面,而不是底层的导航逻辑。 :::
dart
final routes = [
GoRoute(
parentNavigatorKey: parentNavigatorKey,
path: signUpPath,
pageBuilder: (context, state) {
return getPage(
child: const SignUpPage(),
state: state,
);
},
),
GoRoute(
parentNavigatorKey: parentNavigatorKey,
path: signInPath,
pageBuilder: (context, state) {
return getPage(
child: const SignInPage(),
state: state,
);
},
),
GoRoute(
path: detailPath,
pageBuilder: (context, state) {
return getPage(
child: const DetailPage(),
state: state,
);
},
),
];
- 命名路由:命名路由能够提供额外的抽象层,让你的路由更加容易被管理。它允许你集中管理路由逻辑, 使代码更容易维护与重构。
dart
static const String signUpPath = '/signUp';
static const String signInPath = '/signIn';
static const String detailPath = '/detail';
命名路由更容易被管理的原因
- 可读性和可维护性:将路由名称集中管理,可以使代码更加清晰和易于理解。当需要修改或更新路由时,只需在一个地方进行更改,而不是在整个代码库中搜索和替换。
- 减少错误:在代码中直接使用字符串来指定路由可能会导致拼写错误。命名路由通过常量来引用,可以减少这种类型的错误。
- 重构友好:如果将来需要重构路由逻辑,命名路由使得这一过程更加简单。因为路由名称和路径被集中定义,所以重构时只需关注一个文件或代码段。
- 代码自动补全:大多数现代 IDE 支持代码自动补全功能,使用命名路由可以充分利用这一特性,提高开发效率。 :::
- 类型安全:Go Router 在设计时,考虑了类型安全,尽可能减少遇到因为类型不匹配,导致的运行时错误, 使你的应用更加的健壮,可维护。
dart
GoRoute(
path: '/details/:id', // :id is a parameter
pageBuilder: (context, GoRouterState state) {
// Access parameters and enforce type safety using type casting
final id = int.tryParse(state.params['id'] ?? '') ?? -1;
// Given that `id` is now an integer, you can proceed safely
return getPage(
child: DetailsPage(id: id),
state: state,
);
},
),
-
深度链接:深度链接这项功能,允许你通过一个 URL,跳转到你应用的特定画面。Go Router 提供了开箱即用的深度链接, 能够实现更复杂的导航情景,提升用户体验。
-
重定向:Go Router 支持重定向。当碰到一些特殊的判定条件,例如用户权限认证未通过,想让其跳转到登录,重定向功能 就非常的有用。当处理这些复杂路由场景时,重定向提供了额外的灵活性。
dart
GoRoute(
path: '/dashboard',
pageBuilder: (context, state) => MaterialPage(child: DashboardScreen()),
// Redirects to login if not authenticated
redirect: (state) {
if (!isUserAuthenticated()) {
return '/login';
}
return null;
},
),
- 参数传递:Go Router 简化了复杂数据在路由间的传递。这个过程并不需要序列化或者解析,让数据在路由间的交互无缝链接。
安装 Go Router
使用下面的命令,可以添加最近版本的 Go Router 到项目中。
sh
flutter pub add go_router
接下来,就让我们深入路由设置的核心部分:CustomNavigationHelper
类。
初始化与使用单例模式
在 Flutter 中,经常在不同页面不同位置,需要使用路由跳转,所以需要有一个全局导航工具方法, 来提供给不同的位置调用。 这里使用单例模式实现这个导航工具类。
dart
class CustomNavigatorHelper{
static final CustomNavigatorHelper _instance = CustomNavigatorHelper._internal();
static CustomNavigatorHelper get instance => _instance;
factory CustomNavigatorHelper(){
return _instance;
}
CustomNavigatorHelper._internal(){
// Router initialization happens here
}
}
说明:上述代码定义了CustomNavigatorHelper
的单例,这确保该类只能实例化一次,且全局共享。
定义 Navigator Keys
对于高级导航设置,特别是对于处理多Navigators
和Tabs
,Flutter 需要独一无二的Key
。 我们在工具类的开始,定义这些keys
。如果不加这些keys
,那在导航和过渡动画上可能出现问题,所以切勿遗漏。
dart
static final GlobalKey<NavigatorState> parentNavigatorKey = GlobalKey<NavigatorState>();
static final GlobalKey<NavigatorState> homeTabNavigatorKey = GlobalKey<NavigatorState>();
说明:每一个key
都代表了一个独一无二的Navigator
,例如homeTabNavigatorKey
就代表了 Home 页 tab 的导航栈。
路由 Routes 列表声明
Routes 列表决定了应用如何在不同页面之间跳转。routes列表由GoRoute
类的实例构成,让我们来看一个例子:
dart
static const String homePath = "/home";
// inside the CustomNavigatorHelper._internal()
final routes = [
GoRoute(
path: homePath,
pageBuilder:(context,GoRouterState state){
return getPage(
child:const HomePage(),
state:state,
);
}
),
];
说明: HomePath
字符串代表了HomeRoute
的路由,在 routes 列表中,我们把这个字符串,和一个pageBuilder
关联起来。 当跳转到该路由时,pageBuilder
方法将会被调用,返回一个HomePage Widget
。
初始化 Go Router
在定义了 routes 列表后,我们接下来初始化 GoRouter:
dart
static late final GoRouter router;
// ... at the end of the _internal
router = GoRouter(
navigatorKey:parentNavigatorKey,
initialLocation: signUpPath,
routes:routes,
);
说明: GoRouter
接收我们定义的routes
路由列表,初始路由(应用启动后打开的首页页面),以及一个 navigator key。
工具函数:getPage
getPage
是一个工具函数,能够把我们路由的页面widget
,嵌入到MaterialPage
中。
dart
static Page getPage({
required Widget child,
required GoRouterState state,
}){
return MaterialPage({
key:state.pageKey,
child:child,
});
}
说明:这个函数,接收我们需要跳转过去的业务widget
和一个state
,返回一个MaterialPage
包裹我们的业务widget
。 这使得我们的路由定义,更加的清晰可读。这里传入state.pageKey
是非常重要的,可以避免一些在路由切换时的诡异动画和混乱。
使用 CustomNavigatorHelper
最后,在我们的 main app widget 中,我们可以使用CustomNavigatorHelper
来启动我们的应用。应用启动前, 我们一定要初始化路由一次,把路由注册完成。 因为使用了单例模式,通过CustomNavigatorHelper()
或者CustomNavigatorHelper.instance
,都可以实现初始化。
dart
main(){
CustomNavigatorHelper.instance;
runApp(const App());
}
dart
class App extends StatelessWidget {
const App({Key? key}):super(key:key);
@override
Widget build(BuildContext context) {
return MaterialApp.router(
debugShowCheckedModeBanner:false,
routerConfig:CustomNavigatorHelper.router,
);
}
}
说明:在 main 函数中,我们首先初始化了CustomNavigatorHelper
,在 app widget 中,我们使用 MaterialPage.router
构造函数,并给它赋值为我们定义的GoRouter
配置。
高级路由 StatefulShellRoute
StatefulShellBranch
以及 PageBuilder
使用PageBuilder
的StatefulShellRoute.indexedStack
在StatefulShellRoute.indexedStack
命名构造函数中,PageBuilder
是重要的参数之一。 PageBuilder
负责渲染 shell 及其子 widget 的 UI。
dart
StatefulShellRoute.indexedStack(
parentNavigatorKey:parentNavigatorKey,
branches:[
// Branched defined here
],
pageBuilder:(
BuildContext context,
GoRouterState state,
StatefulNavigationShell navigationShell,
){
return getPage(
child:BottomNavigationPage(
child:navigationShell
),
state:state,
);
},
);
说明: 这里,pageBuilder
接收三个参数:
BuildContext context
:构建的上下文GoRouterState state
:当前路由的状态StatefulNavigationShell navigationShell
:保存子分支导航状态的自定义对象
这个函数,返回了一个我们自己的BottomNavigationPage
的页面,这个页面又包含了 navigationShell
。这样,BottomNavigationPage
就变成了一个外壳,包裹着子分支,这样应用底部就固定包含了一个导航组件。 我们也可以在BottomNavigationPage
顶部加一个AppBar
,如此子分支就只占据 body 部分。
在 StatefulShellBranch
的 routes 中,PageBuilder 的作用
在每个 StatefulShellBranch 的 routes 列表中,每个 GoRoute 类,都使用了 PageBuilder 属性,该属性是另一种用途。
dart
StatefulShellBranch(
navigatorKey:homeTabNavigatorKey,
routes:[
GoRoute(
path:homePath,
pageBuilder:(context,GoRouterState state){
return getPage(
child:const HomePage(),
state:state,
);
},
),
],
);
说明:在 StatefulShellBranch
中,pageBuilder
负责定义分支中每个路由的展示画面渲染,如此每个 tab 就有了 自己独立的导航栈。
通过在 StatefulShellRoute.indexedStack
和 StatefulShellBranch
的 Routes 列表的 GoRoute
类实例中,使用 pageBuilder
,我们实现了一个可靠的,模块化的,高效的路由机制,其与底部导航能够完美结合。
总之,StatefulShellRoute.indexedStack
中的 pageBuilder
作为一个外壳,承载了底部导航部件, 而每个 StatefulShellBranch
中的 pageBuilder
,则关注每个 tab 的导航页面栈中的页面。这种分层的方法,可以实现无缝的用户体验。
BottomNavigationPage 和 GoRouter 如何一起工作
现在让我们看下,BottomNavigationPage 类,如何和 CustomNavigationHelper 串联在一起。
BottomNavigationPage 类
BottomNavigationPage
属于StatefulWidget
,其包含了一个底部导航的BottomNavigationBar
组件, 并且以 StatefulNavigationShell
作为其子组件。
dart
class BottomNavigationPage extends StatefulWidget {
const BottomNavigationPage({super.key,required this.child});
final StatefulNavigationShell child;
}
处理导航变化
在 BottomNavigationPage
内部,我们使用 widget.child.goBranch
方法,管理底部导航组件状态的变化。
dart
onTap:(index){
widget.child.goBranch(
index,
initialLocation:index==widget.child.currentIndex,
);
setState((){});
}
说明:当底部导航 tab 被点击时,StatefulNavigationShell
的 child
中的 goBranch
方法被调用。这个方法 会修改当前的分支,从而切换导航堆栈。那之后,调用 setState,触发重新渲染新分支的画面。
二者结合
当配置 StatefulShellRoute.indexedStack
时,把 navigationShell
作为子级,传递给 BottomNavigationPage
的 child
。
dart
pageBuilder:(
BuildContext context,
GoRouteState state,
StatefulNavigationShell navigationShell,
){
return getPage(
child:BottomNavigationPage(
child:navigationShell,
),
state:state
);
}
说明: StatefulShellRoute.indexedStack
的 pageBuilder
返回了一个包含了 BottomNavigationPage
的 MaterialPage
, 这就让 GoRouter 能够无缝管理路由,同时还依然能提供自定义的底部导航体验。
通过结合 CustomNavigationHelper
和 BottomNavigationPage
,你不仅能动态的管理路由, 还能为底部导航栏的每一个 tab,保持独立的路由栈,从而在你的 flutter 应用中,造就一个干净,模块化, 便于管理的导航设置。
作为 shell 外壳的 BottomNavigationPage
什么是 shell 外壳
在应用开发中,shell 一般指的是一个作为外壳的组件,它包裹了其他画面,提供了一组一致的布局或交互。 在我们应用的上下文中,BottomNavigationPage 和 AppBar 都充当了 shell 外壳的构成部分。
关于 BottomNavigationPage
BottomNavigationPage
类是一个 StatefulWidget
,其接收一个 StatefulNavigationShell
作为参数, 该 shell 包含了被 StatefulShellRoute.indexedStack
管理的各个独立分支的导航状态。
dart
class _BottomNavigationPageState extends State<BottomNavigationPage> {
@override
Widget build(BuildContext context){
return Scaffold(
appBar:AppBar(
title:const Text("Bottom Navigator Shell"),
),
body:SafeArea(
child:widget.child,
),
bottomNavigationBar:BottomNavigationBar(
...
),
);
}
}
这里是几个重要组件的说明:
AppBar
AppBar 在不同页面,提供了一个一致的顶部区域。在我们的样例中,它负责管理标题。
Body
body 被设置成了 SafeArea(child:widget.child)
,这里的 widget.child
是被传入的 StatefulNavigationShell
, 它基于被激活的 tab,控制当前的页面,而使用 SafeArea 确保其不会遮盖任何的系统画面。
BottomNavigationBar
BottomNavigationBar 是交互性更强的地方。它不仅负责展示配置的 tab,还负责处理点击 tab 时的切换。
dart
BottomNavigationBar(
type:BottomNavigationBarType.fixed,
currentIndex: widget.child.currentIndex,
onTap:(index){
widget.child.getBranch(
index,
initialLocation: index==widget.child.currentIndex,
);
setState((){});
},
);
这里,currentIndex
是从 StatefulNavigationShell
中获取,用来指示哪一个 tab 处于激活状态。 当底部 tab 被点击时,onTap
方法,就通过调用 StatefulNavigationShell
中的 goBranch
,来切换到合适的分支。 最后调用 setState
,确保重新构建画面以响应分支变化。
总结各个部分如何关联在一起
- AppBar 作为外壳中,顶部固定的组件。
- BottomNavigationBar 负责作为外壳中,底部的固定组件,同时控制不同 tab 间的切换。
- Body 展示当前激活的 tab 对应的页面,从而确保无缝的用户体验。
本质上,BottomNavigationPage 作为一个外壳,把负责导航的部分,和负责独立页面展示的部分,粘和在一起。 它利用 StatefulNavigationShell,来维护和管理导航状态,从而构成了一套简单却又复杂的用户界面。
完整示例
下面的代码片段,提供了一个如何在 Flutter 应用中使用 Go Router 的全面的示例。 该示例,展示了路由定义,StatefulShellRoute,和一些其他的功能特性,例如如何从根路由导航或者从shell外壳路由导航。
你可以在这里查看代码 Github Gist。