首页
代码
lib/main.dart
:
import 'package:flutter/material.dart';
import 'package:get/get_navigation/src/root/get_material_app.dart';
import 'package:getx_quickstart_learn/common/routes/app_pages.dart';
Future<void> main() async {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// GetMaterialApp: getx 里面的组件
return GetMaterialApp(
// 不显示 debug banner 这个图标
debugShowCheckedModeBanner: false,
initialRoute: AppPages.INITIAL,
// 路由表
getPages: AppPages.routes,
);
}
}
lib/common/routes/app_pages.dart
:
import 'package:get/get.dart';
import 'package:getx_quickstart_learn/pages/home/index.dart';
// 用 part / part of,可以把多个文件合成一个库,这样它们之间可以互访私有成员。
// part 表示这是 主库
part 'app_routes.dart';
class AppPages {
static const INITIAL = AppRoutes.Home;
static final routes = [GetPage(name: AppRoutes.Home, page: () => HomeView())];
}
lib/common/routes/app_routes.dart
:
// 用 part / part of,可以把多个文件合成一个库,这样它们之间可以互访私有成员。
// part of 表示 子库
part of 'app_pages.dart';
// 在这里类里面定义路由名字
abstract class AppRoutes {
static const Home = '/home';
}
lib/pages/home/index.dart
:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class HomeView extends StatelessWidget {
const HomeView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("首页")),
body: ListView(
children: [
// 路由&导航
Divider(),
],
),
);
}
}

启动时的执行过程
应用启动:runApp(MyApp())
→ 渲染 MyApp
→ 进入 GetMaterialApp
。
在GetMaterialApp
中
initialRoute
→ 决定应用启动时要显示的初始页面。initialRoute: AppPages.INITIAL
=initialRoute: AppRoutes.Home
=initialRoute: '/home'
getPages
→ 是一个路由表,里面定义了路由名与页面的对应关系。getPages: AppPages.routes
- =
getPages: [GetPage(name: AppRoutes.Home, page: () => HomeView())]
- =
getPages: [GetPage(name: '/home', page: () => HomeView())]
匹配路由表:由于initialRoute
已经设置了首页的名字是'/home'
,那么GetX就会去路由表getPages
里面找,根据名字('/home'
)找到HomeView()
。
page: () => HomeView()
GetPage
:这是一个构造函数,要传入name
和page
两个参数
page
参数接收一个 函数,返回要显示的页面。
() => HomeView()
是一个 匿名函数,每次调用都会新建一个 HomeView
实例。
这么写而不是直接写 page: HomeView()
,有两个好处:
- 延迟加载:只有真正跳转到这个路由时才会创建页面,提升性能。
- 每次新建:每次进入页面都会得到一个新的
HomeView
,不会复用旧的实例。
GetX 的 路由路径 和 文件路径没有关系
路由路径(/home
)是你自己定义的逻辑名称,用来在 GetPage
里匹配跳转。
文件路径(lib/pages/home/index.dart
)只是你存放 HomeView
代码的地方,跟 GetX 的路由机制没有直接联系。
最终显示哪个页面,完全取决于你在 GetPage
里绑定的 page: () => HomeView()
。
GetPage
的 name
本质上就是一个字符串标识,你完全可以写成 /aaa
、/xyz
,甚至 '/🐱'
,只要你前后一致,就能正常跳转。
为什么大多数项目用“路径风格”的名字?
这是一个 约定俗成的架构习惯,主要有这几个原因:
层级结构更直观。比如一个电商应用
/home /product /product/detail /cart /order
这样一看就知道:
detail
是product
的子页面。如果名字随便取(比如/p1
、/p2
),久了根本记不住。和前端 Web 路由统一:Flutter 虽然是移动端,但很多开发者有 Web 背景,
/xxx/yyy
的形式很像浏览器的 URL。这样可以让代码更容易维护、也方便迁移到 Flutter Web。避免冲突:如果大家随便取名字,很可能出现重复的名字。用路径结构命名,规则更统一,降低出错率。
GetPage(name: '/home', page: () => HomeView()), GetPage(name: '/home2', page: () => HomeView()), // 重复含义
为什么首页常用 /home
而不是 /home/index
在很多 前端/后端框架里,
/
就是首页/index
、/index.html
通常是默认入口文件(如果不写,服务器会自动去找index.html
)。
例如:
- Web 框架(Vue / React / Angular):
/
代表首页路由。 - 静态网站服务器(Nginx/Apache):请求
/
时,会默认返回index.html
。 - Node.js / Express:默认路由
app.get('/')
代表首页。
所以 Flutter 的 GetX 在设计路由时,大家也会借用这种约定俗成的习惯:
- 首页 →
/
或/home
- 模块首页 →
/product
(而不是/product/index
) - 子页面 →
/product/detail
这样命名,既符合直觉,也和 Web 的路由习惯保持一致。
嵌套路由
class AppPages {
static const INITIAL = AppRoutes.Home;
static final routes = [
GetPage(
name: AppRoutes.Home, // '/home'
page: () => HomeView(),
children: [
GetPage(
name: AppRoutes.List, // '/list'
page: () => ListView(),
children: [
GetPage(
name: AppRoutes.Detail, // '/detail'
page: () => DetailView(),
),
],
),
],
),
];
}
GetPage
支持定义 嵌套路由( children 属性)。可以理解为一棵 路由树:
/home
└── /list
└── /detail
- Home 是根节点
- List 是 Home 的子节点
- Detail 是 List 的子节点
嵌套路由会自动拼接父节点的路径,所以子路由要写相对路径(不要再写 /home/list/detail
,只写 /detail
就行)。
而当我们实际访问的时候:
// 进入 Home:
Get.toNamed('/home');
// 在 List 里进入 Detail:
Get.toNamed('/home/list/detail');
好处
- 路由更清晰:一看就知道 Detail 属于 List,List 属于 Home
- 方便管理大型项目:树状结构比扁平化更好维护
- 支持 嵌套导航(比如 TabView 里还套子页面)
页面跳转
基本语法
匿名路由(不带名字,直接跳页面实例):
Get.to(DetailView()); // 推入一个新页面 Get.off(DetailView()); // 替换当前页面 Get.offAll(DetailView()); // 清空栈,跳到新页面
命名路由(推荐,方便维护):
Get.toNamed(AppRoutes.Detail); // 跳转到 /detail Get.offNamed(AppRoutes.Detail); // 替换当前 Get.offAllNamed(AppRoutes.Detail); // 清空栈
返回上一级
Get.back(); // 返回上一级
三种跳转方式的区别
从上面的案例中,可以发现有3中跳转方式
推入一个新页面(
Get.toxxx
)[Home] → 初始 [Home, Detail] → Get.to()
路由栈中还留之前的页面,可以返回。
替换当前页面(
Get.offxxxx
)[Home] → 初始 [Detail] → Get.off()
路由栈中当前页面被替换,不能返回原页面。
清空栈,跳到新页面(
Get.offAllxxxxx
)[Home] → 初始 [Detail] → Get.offAll()
清空历史,只剩下目标页面,无法回退。
方法 | 栈变化 | 能否返回上页 |
---|---|---|
Get.to() |
[Home] → [Home, Detail] |
✅ 可以 |
Get.off() |
[Home] → [Detail] |
❌ 不行 |
Get.offAll() |
[Home] → [Detail] (清空栈) |
❌ 不行 |
Get.toNamed() |
[Home] → [Home, Detail] |
✅ 可以 |
Get.offNamed() |
[Home] → [Detail] |
❌ 不行 |
Get.offAllNamed() |
[Home] → [Detail] (清空栈) |
❌ 不行 |
代码
lib/common/routes/app_pages.dart
class AppPages {
static const INITIAL = AppRoutes.Home;
static final routes = [
GetPage(
name: AppRoutes.Home,
page: () => HomeView(), // Home = '/home';
children: [
GetPage(
name: AppRoutes.List, // List = '/list';
page: () => ListView(),
children: [
GetPage(
name: AppRoutes.Detail, // Detail = '/detail'
page: () => DetailView(),
),
],
),
],
),
];
}
lib/pages/home/index.dart
class HomeView extends StatelessWidget {
const HomeView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("首页")),
body: ListView(
children: [
Divider(), // 分割线
// 路由&导航
ListTile(
title: Text("导航-命名路由 home > list"),
subtitle: Text('Get.toNamed("/home/list")'),
onTap: () => Get.toNamed("/home/list"), // 命名路由
),
ListTile(
title: Text("导航-命名路由 home > list > detail"),
subtitle: Text('Get.toNamed("/home/list/detail")'),
onTap: () => Get.toNamed("/home/list/detail"), // 命名路由
),
ListTile(
title: Text("导航-类对象"),
subtitle: Text("Get.to(DetailView())"),
onTap: () => Get.to(DetailView()), // 直接跳转到 DetailView, 匿名路由
),
// 清除上一个路由
ListTile(
title: Text("导航-清除上一个"),
subtitle: Text("Get.off(DetailView())"),
onTap: () => Get.off(DetailView()),
),
// 清除所有路由
ListTile(
title: Text("导航-清除所有"),
subtitle: Text("Get.offAll(DetailView())"),
onTap: () => Get.offAll(DetailView()),
),
],
),
);
}
}

lib/pages/list/index.dart
import 'package:flutter/material.dart';
class ListView extends StatelessWidget {
const ListView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(appBar: AppBar(title: Text("列表页")));
}
}

lib/pages/list_detail/index.dart
class DetailView extends StatelessWidget {
const DetailView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("详情页")),
body: ListView(
children: [
ListTile(
title: Text("导航-返回"),
subtitle: Text('Get.back()'),
onTap: () => Get.back(),
),
],
),
);
}
}

传递参数
基本语法
命名路由带参数
// 跳转时传参
Get.toNamed(AppRoutes.Detail, arguments: {"id": 123, "name": "Flutter"});
// DetailView 页面获取参数
final args = Get.arguments;
print(args['id']); // 123
print(args['name']); // Flutter
URL query 参数
// 跳转
Get.toNamed("/detail?id=123&name=Flutter");
// 页面获取
var id = Get.parameters['id']; // "123"
var name = Get.parameters['name']; // "Flutter"
URL路径参数
路由定义:这里的
:id
表示这是一个动态占位符。GetPage( name: '/detail/:id', page: () => DetailView(), ),
跳转时传参
// 这里 777 会自动填到 :id 的位置。 Get.toNamed('/detail/777'); // 还可以额外带 query 参数 Get.toNamed('/detail/777?name=flutter&age=18');
页面中获取参数
// 路径参数(/detail/:id) final id = Get.parameters['id']; // Query 参数(?name=flutter&age=18) final name = Get.parameters['name']; final age = Get.parameters['age'];
返回时带数据
// 返回时带数据
Get.back(result: {"success": true})
// 获取返回时获取参数
// 使用 async 和 await :在跳转的时候等待Get.toNamed的返回值result,
onTap: () async {
var result = await Get.toNamed("/home/list/detail/777");
Get.snackbar(
"返回值",
"success -> ${result != null ? result["success"] : "成功"} ",
);
},
完整代码
lib/pages/home/index.dart
ListTile(
title: Text("导航-arguments传值+返回值"),
subtitle: Text(
'Get.toNamed("/home/list/detail", arguments: {"id": 999})',
),
onTap: () async {
var result = await Get.toNamed(
"/home/list/detail",
arguments: {"id": 999},
);
Get.snackbar(
"返回值",
"success -> ${result != null ? result["success"] : "成功"}",
);
},
),
ListTile(
title: Text("导航-parameters传值+返回值"),
subtitle: Text('Get.toNamed("/home/list/detail?id=666")'),
onTap: () async {
var result = await Get.toNamed("/home/list/detail?id=666");
Get.snackbar(
"返回值",
"success -> ${result != null ? result["success"] : "成功"}",
);
},
),
ListTile(
title: Text("导航-参数传值+返回值"),
subtitle: Text('Get.toNamed("/home/list/detail/777")'),
onTap: () async {
var result = await Get.toNamed("/home/list/detail/777");
Get.snackbar(
"返回值",
"success -> ${result != null ? result["success"] : "成功"} ",
);
},
),
Get.snackbar
Get.snackbar
是 GetX 提供的一个全局轻量提示组件,它的主要作用是在屏幕的 顶部或底部 弹出一条消息,常用于提示操作结果、警告或通知。
// 基本用法
Get.snackbar(
"标题", // snackbar 的标题
"这是提示内容", // snackbar 的正文
);

lib/pages/list_detail/index.dart
_buildBackListTileRow(Map? val) {
return val == null
? Container()
: ListTile(
title: Text("传值 id = ${val["id"]}"),
subtitle: Text('Get.back(result: {"success": true}'),
onTap: () => Get.back(result: {"success": true}),
);
}
@override
Widget build(BuildContext context) {
Map parameters = (Get.arguments != null)
? (Get.arguments as Map)
: Get.parameters;
return Scaffold(
appBar: AppBar(title: Text("详情页")),
body: ListView(
children: [
ListTile(
title: Text("导航-返回"),
subtitle: Text('Get.back()'),
onTap: () => Get.back(),
),
_buildBackListTileRow(parameters),
],
),
);
}
lib/common/routes/app_pages.dart
GetPage(name: AppRoutes.Detail_ID, page: () => DetailView()),
lib/common/routes/app_routes.dart
static const Detail_ID = '/detail/:id';v
动态参数
有这么一些路径:/home/list/:unknown
,/home/:anything
,/home/:abc
在路径中使用冒号
:
开头的部分表示 动态参数。可以写成
:id
、:abc
、:unknown
、:anything
都可以,名字随便起,本质就是占位符。占位符名字会变成参数的 key,你可以通过
Get.parameters['key']
拿到对应的值。
GetPage(name: "/home/list/:id", page: () => DetailView()),
GetPage(name: "/home/:something", page: () => NotfoundView()),
当你访问 /home/list/123
- 匹配到
/home/list/:id
Get.parameters['id'] == "123"
当你访问 /home/abc
- 匹配到
/home/:something
Get.parameters['something'] == "abc"
当你访问 /home/list/abc
- 如果你没定义
/home/list/:id
,就会走/home/:something
Get.parameters['something'] == "list/abc"
GetX 的匹配规则是 最长路径优先,先找完全匹配,才会回退到动态参数。
GetPage(name: "/home/list", page: () => ListView()),
GetPage(name: "/home/list/:id", page: () => DetailView()),
GetPage(name: "/home/:anything", page: () => NotfoundView()),
/home/list
→ 列表页面
/home/list/123
→ 详情页面
/home/xyz
→ 兜底 notfound
GetX 路由匹配机制
GetX 的路由匹配逻辑基于 Get.routeTree.matchRoute 方法,核心特点如下:
- 前缀匹配:GetX 检查请求路径是否以某个已定义路由开头,选择最长的匹配路由。
- 静态路由优先:静态路由(如
/home/list
)优先级高于动态路由(如/home/:param
)。 - 路径段解析:路径按
/
分割成段,逐段比较以找到匹配的路由。 - 完全匹配优先:如果路径完全匹配某个路由,GetX 直接选择该路由;否则,尝试部分匹配(前缀匹配)。
- 部分匹配:GetX 允许路径以定义的路由开头,忽略额外的段。
- 无匹配时的行为:如果没有匹配的路由(包括前缀匹配),GetX 会触发
unknownRoute
(如果配置了)或抛出错误。
getPages: [
GetPage(name: "/home", page: () => HomeView()),
GetPage(name: "/home/list", page: () => ListView()),
GetPage(name: "/home/list/detail", page: () => DetailView()),
GetPage(name: "/home/:unknown", page: () => NotfoundView()),
],
分析 /home/list/abc 匹配到 /home/list
路径分解:/home/list/abc
分割为 ['home'
, 'list'
, 'abc'
](3 段)。
路由匹配:
- 检查
getPages
中的路由:/home
(1 段:['home'
])/home/list
(2 段:['home'
,'list'
])/home/list/detail
(3 段:['home'
,'list'
,'detail'
])/home/:unknown
(2 段:['home'
,':unknown'
])
/home/list/abc
的前两段(home
,list
)完全匹配/home/list
。/home/list/detail
不匹配,因为第三段abc
不等于detail
。- 这个时候由于
/home/list/abc
的前两段匹配到/home/list
,所有实现了部分匹配 - 不匹配
/home
是因为要遵循最长路径原则。显然/home/list
的路径要更长。 /home/:unknown
理论上可以匹配(:unknown = list/abc
),但静态路由/home/list
的优先级更高。
未知路由
基本使用
在 GetX 的路由管理 中,unknownRoute
用来处理 未知路由(即未定义的路由) 的情况,相当于 Web 开发中的 404 页面。
当使用 Get.toNamed("/someRoute")
跳转时,如果 /someRoute
没有在 GetPage
里定义,就会触发 unknownRoute
,显示你指定的兜底页面。
在 GetMaterialApp 中配置:
GetMaterialApp(
// 不显示 debug banner 这个图标
debugShowCheckedModeBanner: false,
initialRoute: AppPages.INITIAL, // 首页
getPages: AppPages.routes, // 已注册的路由表
unknownRoute: AppPages.unknownRoute, // 未匹配到路由时跳转的页面
);
// AppPages.unknownRoute 的值
static final unknownRoute = GetPage(
name: AppRoutes.NotFound,
page: () => NotfoundView(),
);
定义unknownRoute
的页面
class NotfoundView extends StatelessWidget {
const NotfoundView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("路由没有找到")),
body: ListTile(
title: Text("返回首页"),
subtitle: Text('Get.offAllNamed(AppRoutes.Home)'),
onTap: () => Get.offAllNamed(AppRoutes.Home),
),
);
}
}
模拟找不到正确路径的情况:
ListTile(
title: Text("导航-not found"),
subtitle: Text('Get.toNamed("/abc")'),
onTap: () {
Get.toNamed("/abc");
},
)
当点击ListTile
按钮的时候,页面就会跳转到NotfoundView
unknownRoute 部分匹配的情况
问题引入
ListTile(
title: Text("导航-not found"),
subtitle: Text('Get.toNamed("/home/abc")'),
onTap: () {
Get.toNamed("/home/abc");
},
)
当我们把路径改成/home/abc
的时候,我们发现,当点击ListTile
的时候,页面并没跳转到NotfoundView
的页面。
这是因为/home/abc
这个路径通过部分匹配,匹配到了/home
。由于ListTile
就在/home
的页面,所以表现为刷新页面。
使用:
占位符自定义
// 对于平铺的:
static final routes2 = [
GetPage(name: "/home", page: () => HomeView()),
GetPage(name: "/home/list", page: () => ListView()),
GetPage(name: "/home/list/detail", page: () => DetailView()),
// 除了/home/list之外,所有的/home/* 都会匹配这个
GetPage(name: "/home/:unknown", page: () => NotfoundView()),
];
// 对于Children树形结构
static final routes = [
GetPage(
name: AppRoutes.Home,
page: () => HomeView(), // Home = '/home';
children: [
GetPage(
name: AppRoutes.List, // List = '/list';
page: () => ListView(),
children: [
GetPage(
name: AppRoutes.Detail, // Detail = '/detail'
page: () => DetailView(),
),
GetPage(name: AppRoutes.Detail_ID, page: () => DetailView()),
],
),
// 除了/home/list之外,所有的/home/* 都会匹配这个
GetPage(name: "/:unknown", page: () => NotfoundView()),
],
),
];
中间件:GetMiddleware
是什么
GetMiddleware
是 GetX 提供的路由中间件基类,用于在路由跳转时拦截、修改、重定向或执行一些逻辑。
相当于 拦截器 或 守卫,你可以在页面跳转前检查权限、登录状态、日志埋点等。
class MyMiddleware extends GetMiddleware {
// 可以重写父类的方法:redirect,onPageCalled,onBindingsStart等
}
比如上面,MyMiddleware
继承了GetMiddleware
类,并且重写了父类的方法。如果某一个路由使用到MyMiddleware
这个中间件,就会在页面跳转前,执行MyMiddleware
重写的方法,然后再跳转的相关的页面。
常用方法
priority
表示中间件的优先级。
多个中间件时,会根据
priority
的值决定执行顺序数值越小优先级越高,越早执行
默认值是
0
middlewares: [
RouteAuthMiddleware(priority: 1), // 第二个执行
LogMiddleware(priority: -1), // 第一个执行
]
老版本的GetX需要重写这个属性:
@override
int? priority = 1;
新的版本:在 GetMiddleware 里,priority
不是 @override
的字段,而是父类构造函数的参数。所以需要在 构造函数里传给 super
:
class RouteAuthMiddleware extends GetMiddleware {
// 可以重写父类的方法:redirect,onPageCalled,onBindingsStart等
RouteAuthMiddleware({int priority = 0}) : super(priority: priority);
}
redirect
@override
RouteSettings? redirect(String? route) {
// 比如:如果没登录,强制跳转到登录页
if (!isLogin) {
return const RouteSettings(name: '/login');
}
return null; // null 表示允许继续进入原本的路由
}
- 作用:决定是否重定向到另一个路由。
- 返回
null
→ 继续进入目标路由。 - 返回
RouteSettings(name: "/login")
→ 跳转到/login
。
onPageCalled
在页面被调用时触发,可以在这里修改 GetPage
或打印日志。
@override
GetPage? onPageCalled(GetPage? page) {
print("准备进入页面:${page?.name}");
return page;
}
onBindingsStart
在依赖注入绑定之前调用,可以动态修改绑定。
@override
List<Bindings>? onBindingsStart(List<Bindings>? bindings) {
print("页面绑定开始");
return bindings;
}
onPageBuildStart
页面 widget 构建前触发,可以做一些准备操作。
@override
GetPageBuilder? onPageBuildStart(GetPageBuilder? page) {
print("页面构建前");
return page;
}
onPageBuilt
页面构建后触发,可以修改返回的 widget(例如包裹一层 Scaffold)。
@override
Widget onPageBuilt(Widget page) {
print("页面构建完成");
return page;
}
onPageDispose
页面销毁时触发,可用于清理资源。
@override
void onPageDispose() {
print("页面被销毁");
}
使用方式
只要在 GetPage
配置里挂载就行:
GetPage(
name: '/home',
page: () => HomePage(),
middlewares: [
MyMiddleware(), // 绑定中间件
],
),
完整案例
// 定义一个认证中间件 RouteAuthMiddleware
class RouteAuthMiddleware extends GetMiddleware {
// 构造函数,可以设置执行优先级
RouteAuthMiddleware({int priority = 0}) : super(priority: priority);
// 模拟一个登录状态(实际项目里你可能会从 storage 或 GetX 控制器里取)
bool get isLoggedIn => false; // true 表示已经登录, false 表示未登录
// 核心方法:在路由跳转前调用
@override
RouteSettings? redirect(String? route) {
// 如果没有登录,重定向到登录页面
if (isLoggedIn) {
return null; // 已经登录,允许访问
}
// 延迟1秒后弹提示(提示用户先登录)
Future.delayed(Duration(seconds: 1), () => Get.snackbar("提示", "请先登录APP"));
// 阻止原来的跳转,改为跳到登录页
return RouteSettings(name: AppRoutes.Login);
}
}
// 使用的时候挂在GetPage:middlewares
GetPage(
name: AppRoutes.My,
page: () => MyView(),
middlewares: [RouteAuthMiddleware(priority: 1)],
),