go_router 基于 Navigation 2Flutter 声明式路由, 通过使用声明式路由来降低复杂性, 适用于各种不同的目标平台(mobile、Web、desktop), 是由 Flutter 生态系统委员会选出的表现出最高质量水平的软件包(Flutter Favorites)。

开发环境:

  • Microsoft Windows 10 Enterprise LTSC [Version 10.0.19044.1586], locale zh-CN

  • Flutter 2.10.3 • Channel Stable, Dart 2.16.1

  • Visual Studio Code, 64-bit edition (version 1.66.1)

  • OpenJDK Runtime Environment (build 11.0.11+9-b60-7590822)

  • Android SDK version 30.0.3, Platform android-31, build-tools 30.0.3

新建项目

执行命令,创建新项目:flutter create my_go_router_app

1
2
3
4
5
6
7
8
9
10
11
12
13
PS D:\flutter_repos> flutter create my_go_router_app
Flutter assets will be downloaded from https://storage.flutter-io.cn. Make sure you trust this source!
Creating project my_go_router_app...
Running "flutter pub get" in my_go_router_app... 1,770ms
Wrote 96 files.

All done!
In order to run your application, type:

$ cd my_go_router_app
$ flutter run

Your application code is in my_go_router_app\lib\main.dart.

打开新创建的项目:

1
2
PS D:\flutter_repos> cd .\my_go_router_app\
PS D:\flutter_repos\my_go_router_app> code .

添加新文件

新建文件 lib/views/index_page.dart, 编辑其内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import 'package:flutter/material.dart';

class IndexPage extends StatelessWidget {
const IndexPage({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Go Router')),
body: Column(
children: [
ListTile(
leading: const Icon(Icons.settings_outlined),
title: const Text('设置'),
onTap: () {},
),
],
),
);
}
}

新建文件 lib/views/movie_detail_page.dart, 编辑其内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'package:flutter/material.dart';

class MovieDetailPage extends StatelessWidget {
const MovieDetailPage({Key? key, required this.id}) : super(key: key);

final String id;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Movie Detail')),
body: Center(
child: Text('当前电影ID: $id'),
),
);
}
}

新建文件 lib/views/settings_page.dart, 编辑其内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
import 'package:flutter/material.dart';

class SettingPage extends StatelessWidget {
const SettingPage({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Settings')),
);
}
}

新建文件 lib/views/search_page.dart, 编辑其内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import 'package:flutter/material.dart';

class SearchPage extends StatelessWidget {
const SearchPage({Key? key, required this.query}) : super(key: key);
final String query;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Search')),
body: Center(
child: Text('查找的内容为: $query'),
),
);
}
}

添加 go_router 依赖

go_router 最新版本: https://pub.flutter-io.cn/packages/go_router

打开文件pubspec.yaml, 在 dependencies 中添加 go_router 包:

1
2
3
4
5
6
dependencies:
flutter:
sdk: flutter

cupertino_icons: ^1.0.2
go_router: ^3.0.7 # 添加这一行

创建路由表

新建文件 lib/utils/app_routes.dart, 编辑其内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import 'package:go_router/go_router.dart';

import '../views/index_page.dart';
import '../views/movie_detail_page.dart';
import '../views/search_page.dart';
import '../views/settings_page.dart';

class AppRoutes {
// 用于路径路由(声明式路由)的常量, 路径不包含参数
static const String homePath = '/'; // 根路由
static const String settingPath = '/settings';
static const String movieDetailPath = '/movie_detail';
static const String searchPath = '/search';

// 用于 命名路由的常量
static const String homeNamed = 'home_page';
static const String settingsNamed = 'setting_page';
static const String movieDetailNamed = 'movie_detail_page';
static const String searchNamed = 'search_page';

static GoRouter router = GoRouter(
initialLocation: homePath, // 默认路由, 不指定这一荐时,默认路由为 '/'
routes: [
GoRoute(
// 不传递参数的路由项
name: homeNamed, // 命名路由
path: homePath, // 路径路由
builder: (context, state) => const IndexPage(),
),
GoRoute(
name: settingsNamed,
path: settingPath,
builder: (context, state) => const SettingPage(),
),
GoRoute(
// 传递参数方式1, 参数格式类似URL:/search?query=flutter
name: searchNamed,
path: searchPath, // 问号格式的参数,在路径中不需要包含参数信息

// GoRouter.of(context).pushNamed(AppRoutes.searchNamed, queryParams: {'query': 'abcd'});
// GoRouter.of(context).push('${AppRoutes.searchPath}?query=flutter');
// GoRouter.of(context).go('/search?query=flutter');
builder: (context, state) {
// state.queryParams 接收用问号隔开的参数
final query = state.queryParams['query'];
return SearchPage(query: query!);
},
),
GoRoute(
// 传递参数方式2, 参数格式:/movie_detail/123
name: movieDetailNamed,
path: '$movieDetailPath/:id', // 位置格式的参数,参数要包含在路径中

// GoRouter.of(context).pushNamed(AppRoutes.searchNamed, params: {'query': 'abcd'});
// GoRouter.of(context).push('${AppRoutes.movieDetailPath}/654321');
// GoRouter.of(context).go('/movie_detail/654321');
builder: (context, state) {
// state.params 接收 `/` 隔开的参数(按位置)
final id = state.params['id']!;
return MovieDetailPage(id: id);
},
),
],
);
}

路由初始化

打开 main.dart 文件, 编辑内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import 'package:flutter/material.dart';

import 'utils/app_routes.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);

// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routeInformationParser: AppRoutes.router.routeInformationParser,
routerDelegate: AppRoutes.router.routerDelegate,
);
}
}

使用路由

打开文件 lib/views/index_page.dart, 编辑其内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:my_go_router_app/utils/app_routes.dart';

class IndexPage extends StatelessWidget {
const IndexPage({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Go Router')),
body: Column(
children: [
ListTile(
leading: const Icon(Icons.settings_outlined),
title: const Text('设置'),
onTap: () {
GoRouter.of(context).push(AppRoutes.settingPath);
},
),
ListTile(
leading: const Icon(Icons.find_in_page_outlined),
title: const Text('搜索_path'),
onTap: () {
GoRouter.of(context).push(
// 实际路径:/search?query=flutter
'${AppRoutes.searchPath}?query=flutter',
);
},
),
ListTile(
leading: const Icon(Icons.find_in_page_outlined),
title: const Text('搜索_named'),
onTap: () {
GoRouter.of(context).pushNamed(
AppRoutes.searchNamed,
// queryParams 传递问号隔开的参数
queryParams: {'query': 'abcd'},
);
},
),
ListTile(
leading: const Icon(Icons.details_outlined),
title: const Text('电影详情_path'),
onTap: () {
// 路径传递参数
GoRouter.of(context).push(
// 实际路径: /movie_detail/654321
'${AppRoutes.movieDetailPath}/654321',
);
},
),
ListTile(
leading: const Icon(Icons.details_outlined),
title: const Text('电影详情_named'),
onTap: () {
// 命令路由传递参数
GoRouter.of(context).pushNamed(
AppRoutes.movieDetailNamed,
// params 传递 `/` 隔开的参数
params: {'id': '123456'},
);
},
),
],
),
);
}
}

路由导航

无参数的路径路由

要在页面之间导航,可以使用 GoRouter.goGoRouter.push 方法:

1
2
// navigate using the GoRouter
onTap: () => GoRouter.of(context).go('/page2')
1
2
// navigate using the GoRouter
onTap: () => GoRouter.of(context).push('/page2')

go_router 还提供了一个使用 Dart扩展方法 的简化导航手段。

1
2
// navigate using the GoRouter more easily
onTap: () => context.go('/page2')
1
2
// navigate using the GoRouter more easily
onTap: () => context.push('/page2')

GoRouter.go 方法跳转后不能返回; GoRouter.push 方法会把当前路由压入堆栈,可以通过 GoRouter.of(context).pop() 返回前一页。

无参数的命名路由

1
GoRouter.of(context).goNamed('settings_page');
1
GoRouter.of(context).pushNamed('settings_page');

通过路由传递参数的方式1

参数传递方式1, 参数格式类似URL:/search?query=flutter

1
2
3
4
5
6
7
8
9
GoRoute(       
name: 'search_page',
path: '/search', // 问号格式的参数,在路径中不需要包含参数信息
builder: (context, state) {
// state.queryParams 接收用问号隔开的参数
final query = state.queryParams['query'];
return SearchPage(query: query!);
},
),

state.queryParams 接收用问号隔开的参数

参数传递方式1 的路由跳转的几种写法:

1
2
3
4
GoRouter.of(context).pushNamed('search_page', queryParams: {'query': 'abcd'});
GoRouter.of(context).push('/search?query=mobile');
GoRouter.of(context).goNamed('search_page', queryParams: {'query': 'android'});
GoRouter.of(context).go('/search?query=flutter');

通过路由传递参数的方式2

参数传递方式2, 参数格式类似:/movie_detail/654342

1
2
3
4
5
6
7
8
9
10
11
GoRoute(
// 传递参数方式2, 参数格式:/movie_detail/123
name: 'movie_detail_page',
path: 'movie_detail/:id', // 位置格式的参数,参数要包含在路径中

builder: (context, state) {

final id = state.params['id']!;
return MovieDetailPage(id: id);
},
),

state.params 接收 / 隔开的参数(按位置)

参数传递方式2 的路由跳转的几种写法:

1
2
3
4
GoRouter.of(context).pushNamed('movie_detail_page', Params: {'id': 654342});
GoRouter.of(context).push('movie_detail/234321');
GoRouter.of(context).goNamed('movie_detail_page', Params: {'id': 794341});
GoRouter.of(context).go('/movie_detail/854785');

错误处理

默认情况下,go_router 带有 MaterialAppCupertinoApp 一个默认错误页面,如果不想使用自带的默认错误处理页面。可以通过设置 GoRoutererrorBuilder 参数来替换默认的错误页面。

1
2
3
4
5
6
7
class App extends StatelessWidget {
...
final _router = GoRouter(
...
errorBuilder: (context, state) => ErrorPage(state.error),
);
}