GetX:路由

首页

代码

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:这是一个构造函数,要传入namepage两个参数

page 参数接收一个 函数,返回要显示的页面。

() => HomeView() 是一个 匿名函数,每次调用都会新建一个 HomeView 实例。

这么写而不是直接写 page: HomeView(),有两个好处:

  1. 延迟加载:只有真正跳转到这个路由时才会创建页面,提升性能。
  2. 每次新建:每次进入页面都会得到一个新的 HomeView,不会复用旧的实例。

GetX 的 路由路径 和 文件路径没有关系

路由路径/home)是你自己定义的逻辑名称,用来在 GetPage 里匹配跳转。

文件路径lib/pages/home/index.dart)只是你存放 HomeView 代码的地方,跟 GetX 的路由机制没有直接联系。

最终显示哪个页面,完全取决于你在 GetPage 里绑定的 page: () => HomeView()

GetPagename 本质上就是一个字符串标识,你完全可以写成 /aaa/xyz,甚至 '/🐱',只要你前后一致,就能正常跳转。

为什么大多数项目用“路径风格”的名字?

这是一个 约定俗成的架构习惯,主要有这几个原因:

  1. 层级结构更直观。比如一个电商应用

    /home
    /product
    /product/detail
    /cart
    /order
    

    这样一看就知道:detailproduct 的子页面。如果名字随便取(比如 /p1/p2),久了根本记不住。

  2. 和前端 Web 路由统一:Flutter 虽然是移动端,但很多开发者有 Web 背景,/xxx/yyy 的形式很像浏览器的 URL。这样可以让代码更容易维护、也方便迁移到 Flutter Web。

  3. 避免冲突:如果大家随便取名字,很可能出现重复的名字。用路径结构命名,规则更统一,降低出错率。

    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路径参数

  1. 路由定义:这里的 :id 表示这是一个动态占位符。

    GetPage(
      name: '/detail/:id',
      page: () => DetailView(),
    ),
    
  2. 跳转时传参

    // 这里 777 会自动填到 :id 的位置。
    Get.toNamed('/detail/777');
    
    // 还可以额外带 query 参数
    Get.toNamed('/detail/777?name=flutter&age=18');
    
  3. 页面中获取参数

    // 路径参数(/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.snackbarGetX 提供的一个全局轻量提示组件,它的主要作用是在屏幕的 顶部或底部 弹出一条消息,常用于提示操作结果、警告或通知。

// 基本用法
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

是什么

GetMiddlewareGetX 提供的路由中间件基类,用于在路由跳转时拦截、修改、重定向或执行一些逻辑。
相当于 拦截器守卫,你可以在页面跳转前检查权限、登录状态、日志埋点等。

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)],
),

×

喜欢就点赞,疼爱就打赏