Flutter 学习笔记 (6):路由与导航 —— 从基础 push/pop 到 go_router

1. 前言

在前面的博客中,我们一直只使用单页面(一个 Scaffold)。但真正的 App 必然包含多个页面:列表页 → 详情页 → 设置页 → 返回并传递数据等。

Flutter 中的页面跳转和管理称为路由(Route) ,而管理路由的组件称为导航器(Navigator) 。本篇将从最基础的 Navigator.pushpop 讲起,然后介绍命名路由的集中管理,最后推荐现代化的声明式路由方案 ------ go_router

2. 基础路由:push 和 pop

核心思想Flutter 将页面放入一个栈中, push 添加新页面, pop 移除栈顶页面返回上一页。

示例:从首页跳转到详情页

less 复制代码
Container(
  margin: EdgeInsets.all(10),
  child: ElevatedButton(
    style: ElevatedButton.styleFrom(
      backgroundColor: Colors.blue.shade300,
      foregroundColor: Colors.white,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(10),
      ),
      textStyle: TextStyle(color: Colors.white, fontSize: 12),
    ),
    onPressed: () {
      Navigator.push(
        context,
        MaterialPageRoute(builder: (context) => DetailPage()),
      );
    },
    child: Text('详情页'),
  ),
),

详情页

less 复制代码
import 'package:flutter/material.dart';

class DetailPage extends StatelessWidget {
  const DetailPage({super.key});
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('详情页')),
      body: Center(
        child: ElevatedButton(
          style: ElevatedButton.styleFrom(
            backgroundColor: Colors.blue.shade300,
            foregroundColor: Colors.white,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(10),
            ),
            textStyle: TextStyle(color: Colors.white, fontSize: 12),
          ),
          onPressed: () {
            Navigator.pop(context);
          },
          child: Text('返回'),
        ),
      ),
    );
  }
}

传参到详情页:通过构造函数传递参数。

less 复制代码
import 'package:flutter/material.dart';

class DetailPage extends StatelessWidget {
  final String title;
  final String content;
  const DetailPage({super.key, required this.title, required this.content});
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: Center(
        child: ElevatedButton(
          style: ElevatedButton.styleFrom(
            backgroundColor: Colors.blue.shade300,
            foregroundColor: Colors.white,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(10),
            ),
            textStyle: TextStyle(color: Colors.white, fontSize: 12),
          ),
          onPressed: () {
            Navigator.pop(context);
          },
          child: Text(content),
        ),
      ),
    );
  }
}
less 复制代码
Container(
  margin: EdgeInsets.all(10),
  child: ElevatedButton(
    style: ElevatedButton.styleFrom(
      backgroundColor: Colors.blue.shade300,
      foregroundColor: Colors.white,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(10),
      ),
      textStyle: TextStyle(color: Colors.white, fontSize: 12),
    ),
    onPressed: () {
      Navigator.push(
        context,
        MaterialPageRoute(
          builder: (context) =>
              DetailPage(title: '详情页', content: '这是详情页的内容'),
        ),
      );
    },
    child: Text('详情页'),
  ),
),

从详情页返回数据 :使用 Navigator.pop 带回结果,并在首页使用 async/await 接收。 详情页部分代码

vbnet 复制代码
onPressed: () {
    Navigator.pop(context, '这是详情页返回的内容');
},

主页部分代码

ini 复制代码
onPressed: () async {
  final result = await Navigator.push(
    context,
    MaterialPageRoute(
      builder: (context) =>
          DetailPage(title: '详情页', content: '这是详情页的内容'),
    ),
  );
  if (result != null) {
    setState(() {
      message = result;
    });
  }
},

3. 命名路由:集中管理页面路径

当 App 页面增多时,到处使用 MaterialPageRoute 会导致代码重复,且页面路径散落在各处。命名路由允许你在一个地方注册所有页面,然后通过字符串路径跳转。

步骤 1:在 MaterialApp 中定义 routes

部分代码,完整代码请看最后的gitee链接 复制代码
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const MyHomePage(title: '我的第一篇Flutter'),
      routes: {
        '/greeting': (context) => const Greetingwidget(name: 'Gitrocr'),
        '/changeable': (context) => const Changeabletextwidget(),
        '/user': (context) => const UserPage(),
        '/simple_counter': (context) => const SimpleCounterProvider(),
        '/todo': (context) => TodoPage(),
        '/post_list': (context) => PostListPage(),
        '/post_list_with_provider': (context) => PostListPageWithProvider(),
      },
    );
  }
}

步骤 2:使用 Navigator.pushNamed 跳转

部分代码,完整代码请看最后的gitee链接 复制代码
ElevatedButton(
    style: ElevatedButton.styleFrom(
        backgroundColor: Colors.blue.shade300,
        foregroundColor: Colors.white,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(10),
        ),
        textStyle: TextStyle(color: Colors.white, fontSize: 12),
    ),
    onPressed: () {
        Navigator.pushNamed(context, '/greeting');
    },
    child: Text('欢迎学习 Flutter'),
),

步骤 3:传递参数(使用 arguments)

部分代码,完整代码请看最后的gitee链接 复制代码
onPressed: () {
    Navigator.pushNamed(
      context,
      '/greeting',
      arguments: {'title': 'Gitrocr'},
    );
},
部分代码,完整代码请看最后的gitee链接 复制代码
import 'package:flutter/material.dart';

class Greetingwidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final args =
        ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>;
    final title = args['title'];
    return Scaffold(
      appBar: AppBar(title: Text(title)),
    );
  }
}

命名路由的缺点:参数类型不安全(需要手动转换),且无法轻易实现深层嵌套路由(比如底部导航栏内的页面跳转)。

4. 现代化方案:go_router(推荐)

go_router 是 Flutter 官方推荐的声明式路由包,它解决了命名路由的痛点,支持路径参数、嵌套路由、重定向、过渡动画等。

添加依赖

yaml 复制代码
dependencies:
  go_router: ^14.0.0   # 请使用最新版本

基本用法

php 复制代码
import 'package:go_router/go_router.dart';

// 1. 定义路由配置
final GoRouter _router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      name: 'home',
      builder: (context, state) => HomePage(),
    ),
    GoRoute(
      path: '/detail/:id',    // 路径参数
      name: 'detail',
      builder: (context, state) {
        final id = state.pathParameters['id']!;
        // 也可以传递额外参数
        final extra = state.extra as String?;
        return DetailPage(id: id, extra: extra);
      },
    ),
  ],
);

// 2. 在 MaterialApp 中使用
MaterialApp.router(
  routerConfig: _router,
  // 不再需要 home 和 routes
)

// 3. 跳转
context.go('/detail/123');                    // 替换当前路由
context.push('/detail/456');                  // 压入新页面
context.pushNamed('detail', pathParameters: {'id': '789'}, extra: '自定义数据');

// 4. 返回
context.pop();

嵌套路由(例如底部导航栏 + 多个子页面):

less 复制代码
final GoRouter _router = GoRouter(
  initialLocation: '/home',
  routes: [
    StatefulShellRoute.indexedStack(
      builder: (context, state, navigationShell) {
        return ScaffoldWithNavBar(navigationShell: navigationShell);
      },
      branches: [
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/home',
              name: 'home',
              builder: (context, state) => HomePage(),
            ),
          ],
        ),
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/profile',
              name: 'profile',
              builder: (context, state) => ProfilePage(),
            ),
          ],
        ),
      ],
    ),
  ],
);

然后在 ScaffoldWithNavBar 中使用 navigationShell.currentIndexonTap 来切换底部导航。

优点总结

  • 类型安全的参数传递(路径参数和额外对象)
  • 支持深链、重定向、守卫(权限控制)
  • 与 Flutter Web 完美兼容(路径与 URL 同步)
  • 嵌套路由非常清晰

5. 实战:将之前的帖子列表改为支持详情页

结合我们第五篇的网络请求,为每个帖子添加点击跳转详情页。

第一步:定义详情页

less 复制代码
class PostDetailPage extends StatelessWidget {
  final Post post;

  const PostDetailPage({super.key, required this.post});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(post.title)),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('ID: ${post.id}', style: TextStyle(color: Colors.grey)),
            SizedBox(height: 16),
            Text(post.body, style: TextStyle(fontSize: 16)),
          ],
        ),
      ),
    );
  }
}

第二步:配置 go_router(假设我们已有 Post 模型)

php 复制代码
GoRoute(
  path: '/post/:id',
  name: 'post',
  builder: (context, state) {
    final id = int.parse(state.pathParameters['id']!);
    // 从 Provider 或直接传入 Post 对象
    final post = state.extra as Post?;
    if (post != null) {
      return PostDetailPage(post: post);
    }
    // 如果没有传 extra,可能需要从缓存中根据 id 获取,简化起见先返回一个空页面
    return PostDetailPage(post: Post(id: id, title: '加载中...', body: ''));
  },
)

第三步:在列表项中跳转

css 复制代码
ListTile(
  title: Text(post.title),
  subtitle: Text(post.body),
  onTap: () {
    context.pushNamed('post', pathParameters: {'id': post.id.toString()}, extra: post);
  },
)

第四步:支持返回刷新 (如果需要列表页在返回时刷新数据,可以在详情页 pop 之前调用某些回调,或使用 context.push 后等待 context.pop 的返回值)。

6. 路由传参对比总结

方式 适用场景 优点 缺点
构造函数 + MaterialPageRoute 简单项目,页面少 直观,类型安全 耦合度高,难以统一管理
命名路由 + arguments 中型项目 集中管理,解耦 参数类型不安全,需要手动解析
go_router 中大型项目,需要嵌套、深链 声明式,类型安全,功能全面 学习成本稍高,需要额外依赖

7. 常见问题与避坑

问题 原因 解决
Navigator.pop 后数据没传回来 没有在 pop 中传递参数,或没有用 await 接收 确保 pop 时带参数,且用 async 等待
使用 go_router 时 context.pop 不工作 context 不是路由上下文 确保调用 pop 的组件在 MaterialApp.router 之下,且使用了正确的 BuildContext
页面跳转动画不想要 默认有左右滑动动画 在 go_router 中可设置 transitionsBuilder 自定义,或用 PageRouteBuilder
深层嵌套路由返回时跳过了中间页面 使用了 go 而不是 push go 会替换整个栈,push 才会压栈;返回请用 pop

8. 小结与作业

本篇我们学习了:

  • 基础路由:Navigator.push / pop,传参与返回数据
  • 命名路由:集中管理页面路径,使用 arguments 传参
  • go_router:现代化的声明式路由,支持路径参数、嵌套路由、类型安全

作业(巩固练习)

  1. 将第五篇的帖子列表改造成使用 go_router,并实现点击进入详情页。
  2. 添加一个设置页面,通过命名路由(或 go_router)跳转,并在设置页面中修改主题颜色或字体大小,返回后主页面样式改变(提示:可使用 Provider 共享主题状态)。
  3. 实现一个简单的登录流程:未登录时自动重定向到登录页,登录成功后才能访问首页。(go_router 的 redirect 功能)

掌握了路由与导航,你已经能够组织起多页面的复杂 App 了。下一篇博客预告:本地存储 ------ 使用 SharedPreferences 和 sqflite 保存数据,实现用户偏好设置和离线缓存。

相关推荐
勤劳打代码1 小时前
翻江倒海——滚动布局下拉视图管理
flutter·前端框架·开源
风华圆舞1 天前
在 Flutter 鸿蒙项目里接入语音识别的完整思路
flutter·语音识别·harmonyos
风华圆舞1 天前
鸿蒙 + Flutter 下如何让 HarmonyOS 能力真正服务于 AI 体验
人工智能·flutter·harmonyos
BreezeDove1 天前
【Android】Flutter3.35项目启动超时问题
android·flutter
风华圆舞1 天前
鸿蒙 MICROPHONE 权限在 Flutter 项目里怎么处理
flutter·华为·harmonyos
愚者Pro2 天前
切换本地 Flutter SDK 版本
flutter
TT_Close2 天前
别再复制旧 Flutter 工程了,真正拖慢你的不是业务代码
flutter·npm·visual studio code
风华圆舞2 天前
鸿蒙 + Flutter 下 AI 助手为什么要支持流式输出
人工智能·flutter·harmonyos
风华圆舞2 天前
鸿蒙 + Flutter 下 AI 页面的状态协同设计
人工智能·flutter·harmonyos