一文掌握Flutter路由管理:深入浅出

前言:Flutter 应用中,路由(Routing)即页面导航,是构建多页面体验的核心能力。从最简单的 Navigator.push,到命名路由、参数传递、返回结果,再到集中式路由管理、深层嵌套与 Navigator 2.0(Router API),本文将以“从零到一”的思路,配合代码示例图解,帮助你快速掌握 Flutter 的路由管理技巧。

目录

  1. Flutter 路由基础:Navigator 1.0

    • 1.1 Navigator.pushNavigator.pop
    • 1.2 参数传递与返回结果
  2. 命名路由与集中式路由表

    • 2.1 在 MaterialApp 中配置 routes
    • 2.2 使用 onGenerateRoute 实现动态路由
    • 2.3 参数解读与示例
  3. 路由观察与页面生命周期

    • 3.1 RouteObserverRouteAware
    • 3.2 页面进入/退出回调场景
  4. 路由守卫与拦截(Route Guard)

    • 4.1 登录鉴权方案示例
    • 4.2 利用 onGenerateRoutearguments 实现守卫
  5. 嵌套路由与多 Navigator 场景

    • 5.1 底部导航栏与独立导航栈
    • 5.2 TabBar + IndexedStack + 子 Navigator
    • 5.3 图解示意
  6. Navigator 2.0(Router API)简介

    • 6.1 为什么要 Navigator 2.0
    • 6.2 核心概念:RouterRouteInformationParserRouterDelegate
    • 6.3 简单示例:URL 与页面状态同步
  7. 实战示例:构建一个简单的登录—主页—详情三层导航

    • 7.1 功能需求与思路
    • 7.2 代码实现:Navigator 1.0 版本
    • 7.3 进阶:Navigator 2.0 版本(Router API)
  8. 总结与最佳实践

一、Flutter 路由基础:Navigator 1.0

在早期的 Flutter 中,最常见的页面跳转方式即基于 Navigator 1.0 API:Navigator.pushNavigator.pop

1.1 Navigator.pushNavigator.pop

  • Navigator.push:在页面栈顶压入一个新路由(新页面)。
  • Navigator.pop:从页面栈顶弹出当前路由,回到上一个页面(或传递返回值)。
// MainPage.dart
import 'package:flutter/material.dart';
import 'detail_page.dart';

class MainPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('MainPage')),
      body: Center(
        child: ElevatedButton(
          child: const Text('跳转到 DetailPage'),
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (ctx) => DetailPage()),
            );
          },
        ),
      ),
    );
  }
}

// DetailPage.dart
import 'package:flutter/material.dart';

class DetailPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('DetailPage')),
      body: Center(
        child: ElevatedButton(
          child: const Text('返回 MainPage'),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
    );
  }
}
  • 运行流程

    1. 应用启动后,MainPage 显示。
    2. 点击按钮后,Navigator.pushDetailPage 放入堆栈,屏幕切换到 DetailPage
    3. DetailPage 中点击“返回”,执行 Navigator.pop(context),该路由出栈,自动回到 MainPage

路由栈示意图(ASCII)

初始状态:
┌────────────┐
│ MainPage   │   ← 当前显示
└────────────┘

点击 push:
┌────────────┐
│ DetailPage │   ← 当前显示
└────────────┘
┌────────────┐
│ MainPage   │
└────────────┘

点击 pop:
┌────────────┐
│ MainPage   │   ← 当前显示
└────────────┘

1.2 参数传递与返回结果

1.2.1 从 MainPage 传参数到 DetailPage

// MainPage.dart
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (_) => DetailPage(message: 'Hello, Detail!'),
  ),
);
// DetailPage.dart
class DetailPage extends StatelessWidget {
  final String message;
  DetailPage({required this.message});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('DetailPage')),
      body: Center(
        child: Text('接收到的参数:$message'),
      ),
    );
  }
}
  • 重点:通过路由构造函数接收外部传递的参数。

1.2.2 从 DetailPage 返回带结果值到 MainPage

// MainPage.dart (修改 onPressed 部分)
onPressed: () async {
  final result = await Navigator.push(
    context,
    MaterialPageRoute(builder: (_) => DetailPage()),
  );
  // 接收返回值
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text('DetailPage 返回:$result')),
  );
},
// DetailPage.dart (在返回按钮中传递结果)
ElevatedButton(
  child: const Text('返回并带参'),
  onPressed: () {
    Navigator.pop(context, '这个是返回值');
  },
),
  • 流程

    1. MainPage 通过 await Navigator.push(...) 等待 DetailPage 出栈并返回一个值。
    2. DetailPage 调用 Navigator.pop(context, someValue) 时,将 someValue 传回给上一个页面。
    3. MainPage 拿到 result 后,可以做后续逻辑(如弹窗、更新状态等)。

二、命名路由与集中式路由表

随着页面增多,若应用中到处写 MaterialPageRoute(builder: …),会显得冗余且难维护。于是可以使用命名路由(named routes)和集中式路由表,由 MaterialApp 一次性注册所有路由,并用名称跳转。

2.1 在 MaterialApp 中配置 routes

// main.dart
import 'package:flutter/material.dart';
import 'pages/home_page.dart';
import 'pages/login_page.dart';
import 'pages/profile_page.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '命名路由示例',
      initialRoute: '/',  // 默认路由
      routes: {
        '/': (context) => const HomePage(),
        '/login': (context) => const LoginPage(),
        '/profile': (context) => const ProfilePage(),
      },
    );
  }
}
  • 路由表

    routes: {
      '/': (context) => const HomePage(),
      '/login': (context) => const LoginPage(),
      '/profile': (context) => const ProfilePage(),
    }
    • /:代表应用启动后的初始页面。
    • /login/profile:其余命名路由。

在页面中跳转

// HomePage.dart
ElevatedButton(
  onPressed: () {
    Navigator.pushNamed(context, '/login');
  },
  child: const Text('去登录'),
),
  • Navigator.pushNamed(context, '/login'):根据路由表,将 /login 对应的 LoginPage 推入栈顶。

2.2 使用 onGenerateRoute 实现动态路由

当希望在跳转时传递参数、或者根据某些条件决定跳转目标时,可使用 onGenerateRoute 回调,手动创建路由。

// main.dart
MaterialApp(
  initialRoute: '/',
  onGenerateRoute: (RouteSettings settings) {
    final name = settings.name;
    final args = settings.arguments;

    if (name == '/') {
      return MaterialPageRoute(builder: (_) => const HomePage());
    } else if (name == '/detail') {
      // 从 arguments 中取出传递的参数
      final id = args as int;
      return MaterialPageRoute(builder: (_) => DetailPage(id: id));
    } else {
      // 未知路由,跳转到 404 页面
      return MaterialPageRoute(builder: (_) => const NotFoundPage());
    }
  },
)
  • RouteSettings:包含 name(路由名)和 arguments(动态传参)。
  • 示例跳转并传参

    Navigator.pushNamed(context, '/detail', arguments: 42);
  • onGenerateRoute 中判断

    if (settings.name == '/detail') {
      final id = settings.arguments as int;
      return MaterialPageRoute(builder: (_) => DetailPage(id: id));
    }

2.3 参数解读与示例

// DetailPage.dart
class DetailPage extends StatelessWidget {
  final int id;
  const DetailPage({required this.id});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('DetailPage')),
      body: Center(child: Text('详情 ID:$id')),
    );
  }
}
  • 跳转

    ElevatedButton(
      onPressed: () {
        Navigator.pushNamed(context, '/detail', arguments: 123);
      },
      child: const Text('查看 ID=123 的详情'),
    );
  • 路由栈示意

    HomePage → (Navigator.pushNamed '/detail', arguments=123) → DetailPage(id=123)

三、路由观察与页面生命周期

在某些场景下,需要监听页面何时被 push、pop、onResume、onPause 等,例如埋点、分析统计、清理资源等。这时可以借助 RouteObserverRouteAware

3.1 RouteObserverRouteAware

  • RouteObserver:一个拥有回调的路由观察者,需在 MaterialApp 中注册。
  • RouteAware:页面(Widget)实现该接口,即可接收路由切换事件。

3.1.1 在 MaterialApp 中注册 RouteObserver

// main.dart
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // ...
      navigatorObservers: [routeObserver],  // 注册路由观察器
      // ...
    );
  }
}

3.1.2 页面实现 RouteAware

// ProfilePage.dart
import 'package:flutter/material.dart';

class ProfilePage extends StatefulWidget {
  const ProfilePage();
  @override
  _ProfilePageState createState() => _ProfilePageState();
}

class _ProfilePageState extends State<ProfilePage> with RouteAware {
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // 在页面依赖更改时,注册订阅
    MyApp.routeObserver.subscribe(this, ModalRoute.of(context)! as PageRoute);
  }

  @override
  void dispose() {
    // 注销订阅
    MyApp.routeObserver.unsubscribe(this);
    super.dispose();
  }

  @override
  void didPush() {
    // 当前路由被 push 到栈顶(首次进入)
    debugPrint('ProfilePage: didPush');
  }

  @override
  void didPopNext() {
    // 栈顶路由 popped,当前路由重新可见(相当于 resume)
    debugPrint('ProfilePage: didPopNext (resumed)');
  }

  @override
  void didPushNext() {
    // 当前路由被新的路由覆盖(相当于 pause)
    debugPrint('ProfilePage: didPushNext (paused)');
  }

  @override
  void didPop() {
    // 当前路由被 pop
    debugPrint('ProfilePage: didPop');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('ProfilePage')),
      body: const Center(child: Text('个人中心页面')),
    );
  }
}
  • 关键回调

    • didPush():页面第一次被 push 时调用。
    • didPopNext():当下一个页面 pop 后,当前页面重新可见时调用。
    • didPushNext():当当前页面上再 push 出新页面,当前页面不可见时调用。
    • didPop():当当前页面被 pop 时调用。

3.2 页面进入/退出回调场景

  • 场景 1:数据埋点
    didPush() 中触发“页面露出”统计,在 didPop() 中触发“页面消失”统计。
  • 场景 2:播放/暂停资源
    如果页面里有视频播放器,didPush() 开始播放,didPushNext() 暂停播放;didPopNext() 恢复播放。
  • 场景 3:实时刷新
    当用户在 A 页面点击按钮跳到 B 页面,并在 B 页面做了设置,回到 A 页面时需要刷新列表,可在 A 的 didPopNext() 中触发网络请求。

四、路由守卫与拦截(Route Guard)

在实际项目中,经常需要在用户未登录时,限制访问某些页面;或根据某些权限动态决定跳转目标。这时就需要在路由层做守卫拦截

4.1 登录鉴权方案示例

  • 思路

    1. 用户点击“进入个人中心”时,先判断是否已登录。
    2. 若未登录,则跳转到登录页(/login)。
    3. 登录成功后,再跳回“个人中心”或继续原先请求。

4.1.1 在 onGenerateRoute 中实现

// main.dart
import 'package:flutter/material.dart';

bool isLoggedIn = false; // 模拟登录状态

Route<dynamic> onGenerateRoute(RouteSettings settings) {
  final name = settings.name;
  final args = settings.arguments;
  
  // 如果要进入 /profile,需要先判断登录状态
  if (name == '/profile') {
    if (!isLoggedIn) {
      // 未登录,先去登录页,并把目标页面信息放在 arguments 中,登录完成后再跳转
      return MaterialPageRoute(
        builder: (_) => LoginPage(targetRoute: '/profile'),
      );
    }
    // 已登录,正常展示 ProfilePage
    return MaterialPageRoute(builder: (_) => const ProfilePage());
  }
  
  // 登录页
  if (name == '/login') {
    final target = (args as String?);
    return MaterialPageRoute(builder: (_) => LoginPage(targetRoute: target));
  }
  
  // 默认路由
  return MaterialPageRoute(builder: (_) => const HomePage());
}

class LoginPage extends StatelessWidget {
  final String? targetRoute;
  const LoginPage({this.targetRoute});
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('LoginPage')),
      body: Center(
        child: ElevatedButton(
          child: const Text('模拟登录'),
          onPressed: () {
            isLoggedIn = true; // 登录成功
            // 登录成功后,跳转到目标路由(如果有)
            if (targetRoute != null) {
              Navigator.pushReplacementNamed(context, targetRoute!);
            } else {
              Navigator.pop(context);
            }
          },
        ),
      ),
    );
  }
}
  • 流程

    1. 当用户 Navigator.pushNamed(context, '/profile') 时,onGenerateRoute 发现 isLoggedIn = false,先跳到 LoginPage,并记录目标路由 '/profile'
    2. LoginPage 中,点击“模拟登录”后,设 isLoggedIn=true,再执行 pushReplacementNamed('/profile'),直接替换当前登录页,进入个人中心。

4.2 利用 onGenerateRoutearguments 实现守卫

  • 示例中重点:通过 RouteSettings.arguments 将“原始目标路由”传递给登录页。
  • 扩展思路:你也可以在 onGenerateRoute 里判断用户权限等级、角色等,决定能否访问某些敏感页面。

五、嵌套路由与多 Navigator 场景

在一些复杂 UI(如带底部导航栏、TabBar)中,需要在各个 Tab 内维护独立的导航栈,这时就要用到嵌套路由

5.1 底部导航栏与独立导航栈

  • 场景:一个含有底部导航的应用,共有 3 个 Tab(Home、Discovery、Profile)。希望切换 Tab 时,各自的页面栈保持独立状态(例如:在 HomeTab 内从 A → B 页面,然后切到 Discovery,再切回 Home 时仍然在 B 页面)。

5.1.1 核心思想

  • 在最外层的 Scaffold 里放置一个 IndexedStack,用于保持各个子 Navigator 的状态。
  • 为每个 Tab 创建一个独立的 Navigator,并用 GlobalKey<NavigatorState> 来管理它的导航操作。
// tab_navigator.dart
import 'package:flutter/material.dart';

class TabNavigator extends StatelessWidget {
  final GlobalKey<NavigatorState> navigatorKey;
  final String tabItem; // 'home', 'discover', 'profile'

  const TabNavigator({required this.navigatorKey, required this.tabItem});

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      onGenerateRoute: (RouteSettings settings) {
        Widget page;
        if (settings.name == '/') {
          switch (tabItem) {
            case 'home':
              page = const HomeTabPage();
              break;
            case 'discover':
              page = const DiscoverTabPage();
              break;
            case 'profile':
              page = const ProfileTabPage();
              break;
            default:
              page = const HomeTabPage();
          }
        } else if (settings.name == '/detail') {
          final args = settings.arguments as String;
          page = DetailPage(data: args);
        } else {
          page = const HomeTabPage();
        }
        return MaterialPageRoute(builder: (_) => page);
      },
    );
  }
}
// main.dart (底部导航 + IndexedStack)
import 'package:flutter/material.dart';
import 'tab_navigator.dart';

class MainScaffold extends StatefulWidget {
  const MainScaffold();
  @override
  _MainScaffoldState createState() => _MainScaffoldState();
}

class _MainScaffoldState extends State<MainScaffold> {
  int _currentIndex = 0;
  // 为每个 Tab 准备一个 NavigatorKey
  final Map<int, GlobalKey<NavigatorState>> _navigatorKeys = {
    0: GlobalKey<NavigatorState>(),
    1: GlobalKey<NavigatorState>(),
    2: GlobalKey<NavigatorState>(),
  };

  void _onTap(int index) {
    if (_currentIndex == index) {
      // 若点中当前 Tab,且该栈不在根页,则 pop 到根
      _navigatorKeys[index]!
          .currentState!
          .popUntil((route) => route.isFirst);
    } else {
      setState(() {
        _currentIndex = index;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: [
          TabNavigator(navigatorKey: _navigatorKeys[0]!, tabItem: 'home'),
          TabNavigator(navigatorKey: _navigatorKeys[1]!, tabItem: 'discover'),
          TabNavigator(navigatorKey: _navigatorKeys[2]!, tabItem: 'profile'),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: _onTap,
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
          BottomNavigationBarItem(icon: Icon(Icons.search), label: '发现'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
        ],
      ),
    );
  }
}
  • 要点

    • IndexedStack:保持子 Widget 状态不被销毁。
    • 独立 Navigator:每个 Tab 内维护自己的路由栈,互不干扰。
    • 点击同一 Tab 时,如果子路由栈深度不为 1,则自动 pop 回根页面。

5.2 TabBar + IndexedStack + 子 Navigator

  • 当顶部使用 TabBar,但仍想保持每个 Tab 的导航状态,则思路几乎一致:只是将 BottomNavigationBar 换成 TabBar,并配合 TabController
  • 利用 DefaultTabController 包裹整个 Scaffold,然后在 TabBarView 每个子页面使用独立 Navigator
// tabbar_navigator.dart
DefaultTabController(
  length: 3,
  child: Scaffold(
    appBar: AppBar(
      title: const Text('TabBar 嵌套路由'),
      bottom: const TabBar(
        tabs: [
          Tab(text: '新闻'),
          Tab(text: '图片'),
          Tab(text: '设置'),
        ],
      ),
    ),
    body: TabBarView(
      children: [
        TabNavigator(navigatorKey: _navigatorKeys[0]!, tabItem: 'news'),
        TabNavigator(navigatorKey: _navigatorKeys[1]!, tabItem: 'gallery'),
        TabNavigator(navigatorKey: _navigatorKeys[2]!, tabItem: 'settings'),
      ],
    ),
  ),
);

5.3 图解示意

┌───────────────────────────────────────────┐
│         MainScaffold (Scaffold)         │
│   ┌───────────────────────────────┐       │
│   │       IndexedStack            │       │
│   │ ┌──────────┐ ┌──────────┐ ┌────────┐ │
│   │ │Navigator │ │Navigator │ │Navigator│ │
│   │ │ (Home)   │ │ (Disc.)  │ │ (Prof.) │ │
│   │ └──────────┘ └──────────┘ └────────┘ │
│   └───────────────────────────────┘       │
│  BottomNavigationBar (3 个 Tab)          │
└───────────────────────────────────────────┘
  • 说明

    • IndexedStack 的第 0 个子 Widget 是 Home Navigator,里面的路由栈可包含 /home/detail/home/settings 等。
    • 切换到第 1 个 Tab 时,会在第二个 Navigator 中显示对应页面,并且不销毁第 0 个 Navigator 的状态。

六、Navigator 2.0(Router API)简介

自 Flutter 1.22 起,官方推出了全新的 Navigator 2.0(Router API),用于更好地支持Web URL 路由深度链接(Deep Link)灵活可控的路由栈。即使今天大多数移动 App 仍可用 Navigator 1.0,但对于需要与浏览器 URL 同步、或需要在恢复时重建路由栈的场景,Navigator 2.0 更具优势。

6.1 为什么要 Navigator 2.0

  • URL 与页面状态双向绑定:在 Web 端,用户可以通过输入网址直接访问某个子页面,Router API 可以根据浏览器 URL 初始化路由。
  • 可编程式路由栈控制:可以以声明式方式描述“当前应显示哪些页面”,无需手动 push/pop,更容易实现复杂场景。
  • 页面恢复与深度链接:当 App 被系统杀死后重启,Router 可以根据原始路由信息自动“回到”之前的路由栈。

6.2 核心概念:RouterRouteInformationParserRouterDelegate

  1. RouteInformationParser

    • 负责解析浏览器地址栏RouteInformation)为应用内部可理解的“路由配置模型”(如一个枚举或自定义对象)。
    • 例如将路径 /profile/123 解析到 MyRoutePath.profile(123) 这样的模型。
  2. RouterDelegate

    • 根据“路由配置模型”来构建实际的页面栈(List)。
    • 需要实现:

      • List<Page> get currentConfiguration:返回当前路由模型,用于同步给 RouteInformationParser
      • Widget build(BuildContext context):构建一个 Navigator,并把 pages 列表传入,决定了实际的页面栈结构。
      • Future<bool> popRoute():当按下 Android 返回键或浏览器后退时,处理 pop 操作并更新路由模型。
  3. Router Widget

    • 用来实际渲染,由 Router.routerDelegateRouter.routeInformationParser 配置,并在底层管理 Navigator

6.3 简单示例:URL 与页面状态同步

以下示例演示一个简单的三页应用(Home → Profile(userId) → Settings),并将路径与页面状态关联。

6.3.1 定义路由模型

// route_path.dart
abstract class MyRoutePath {}

class HomePath extends MyRoutePath {}

class ProfilePath extends MyRoutePath {
  final String userId;
  ProfilePath(this.userId);
}

class SettingsPath extends MyRoutePath {}

6.3.2 实现 RouteInformationParser

// my_route_parser.dart
import 'package:flutter/material.dart';
import 'route_path.dart';

class MyRouteInformationParser extends RouteInformationParser<MyRoutePath> {
  @override
  Future<MyRoutePath> parseRouteInformation(
      RouteInformation routeInformation) async {
    final uri = Uri.parse(routeInformation.location ?? '/');

    // /profile/123
    if (uri.pathSegments.length == 2 && uri.pathSegments[0] == 'profile') {
      final userId = uri.pathSegments[1];
      return ProfilePath(userId);
    }

    // /settings
    if (uri.path == '/settings') {
      return SettingsPath();
    }

    // 默认 /
    return HomePath();
  }

  @override
  RouteInformation restoreRouteInformation(MyRoutePath configuration) {
    if (configuration is ProfilePath) {
      return RouteInformation(location: '/profile/${configuration.userId}');
    }
    if (configuration is SettingsPath) {
      return const RouteInformation(location: '/settings');
    }
    return const RouteInformation(location: '/');
  }
}

6.3.3 实现 RouterDelegate

// my_router_delegate.dart
import 'package:flutter/material.dart';
import 'route_path.dart';
import 'pages/home_page.dart';
import 'pages/profile_page.dart';
import 'pages/settings_page.dart';

class MyRouterDelegate extends RouterDelegate<MyRoutePath>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<MyRoutePath> {
  @override
  final GlobalKey<NavigatorState> navigatorKey;

  MyRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();

  MyRoutePath _currentPath = HomePath();

  MyRoutePath get currentConfiguration => _currentPath;

  // 更新路由模型并通知重建
  void _handlePath(MyRoutePath newPath) {
    _currentPath = newPath;
    notifyListeners();
  }

  // 处理 Android 实体返回键 / 浏览器后退
  @override
  Future<bool> popRoute() {
    if (_currentPath is ProfilePath || _currentPath is SettingsPath) {
      _currentPath = HomePath();
      notifyListeners();
      return Future.value(true);
    }
    return Future.value(false); // 不能 pop,交给系统
  }

  @override
  Widget build(BuildContext context) {
    // 根据 _currentPath 构建 Navigator.pages 列表
    final pages = <Page>[
      MaterialPage(child: HomePage(onProfile: (userId) {
        _handlePath(ProfilePath(userId));
      }, onSettings: () {
        _handlePath(SettingsPath());
      })),
    ];

    if (_currentPath is ProfilePath) {
      final userId = (_currentPath as ProfilePath).userId;
      pages.add(MaterialPage(child: ProfilePage(userId: userId)));
    }

    if (_currentPath is SettingsPath) {
      pages.add(MaterialPage(child: SettingsPage()));
    }

    return Navigator(
      key: navigatorKey,
      pages: pages,
      onPopPage: (route, result) {
        if (!route.didPop(result)) {
          return false;
        }
        // 用户在页面上点击返回,更新路由模型
        if (_currentPath is ProfilePath || _currentPath is SettingsPath) {
          _currentPath = HomePath();
          notifyListeners();
        }
        return true;
      },
    );
  }

  @override
  Future<void> setNewRoutePath(MyRoutePath configuration) {
    _currentPath = configuration;
    return Future.value();
  }
}

6.3.4 将 Router 挂载到 App

// main.dart
import 'package:flutter/material.dart';
import 'my_route_parser.dart';
import 'my_router_delegate.dart';

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

class MyApp extends StatelessWidget {
  const MyApp();

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routeInformationParser: MyRouteInformationParser(),
      routerDelegate: MyRouterDelegate(),
      title: 'Navigator 2.0 示例',
    );
  }
}
  • 说明

    • MaterialApp.router 指定了 RouteInformationParserRouterDelegate
    • 当用户在浏览器地址栏输入 /profile/123 时,parseRouteInformation 会返回 ProfilePath('123'),随后 RouterDelegate 将该路径映射到相应页面栈,并展示 ProfilePage(userId: '123')
    • 当在页面内部调用 _handlePath(SettingsPath()) 时,currentConfiguration 会被更新为 /settings,自动同步到浏览器地址栏。

七、实战示例:构建一个简单的登录—主页—详情三层导航

下面以一个典型的“登录—主页—详情”示例,分别用 Navigator 1.0 与 Navigator 2.0 两种方式,实现路由管理,并对比两者的差异与优劣。

7.1 功能需求与思路

  1. 需求

    • 用户打开应用后进入 LoginPage
    • 登录成功后,跳转到 HomePage
    • HomePage 有一个列表,点击某行可进入 DetailPage(itemId)
    • 支持从 DetailPage 返回到 HomePage,并支持按物理“后退”键返回或退出应用。
    • 支持深度链接:如果用户直接通过浏览器访问 /detail/42,且已登录,则直接进入 DetailPage(itemId=42);如果未登录,则先进入 LoginPage,登录成功后再自动跳转到该 DetailPage
  2. 思路

    • Navigator 1.0:使用 onGenerateRoute 做登录保护、参数解析,并在 LoginPage 成功后手动管理跳转栈。深度链接支持不友好,需要外部插件或手动解析 initialRoute
    • Navigator 2.0:通过 RouteInformationParser 解析 URL,内置深度链接支持;RouterDelegate 统一根据登录状态和目标路由构建页面栈,逻辑更清晰、可维护。

7.2 代码实现:Navigator 1.0 版本

7.2.1 定义页面文件

// login_page.dart
import 'package:flutter/material.dart';

bool isLoggedIn = false; // 全局模拟登录状态

class LoginPage extends StatelessWidget {
  final String? targetRoute;
  const LoginPage({this.targetRoute});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Login')),
      body: Center(
        child: ElevatedButton(
          child: const Text('登录'),
          onPressed: () {
            isLoggedIn = true;
            // 登录后,如果有原始目标路由,则跳转该路由
            if (targetRoute != null) {
              Navigator.pushReplacementNamed(context, targetRoute!);
            } else {
              Navigator.pushReplacementNamed(context, '/home');
            }
          },
        ),
      ),
    );
  }
}

// home_page.dart
import 'package:flutter/material.dart';

class HomePage extends StatelessWidget {
  const HomePage();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: ListView.builder(
        itemCount: 100,
        itemBuilder: (ctx, i) {
          return ListTile(
            title: Text('Item $i'),
            onTap: () {
              Navigator.pushNamed(
                context,
                '/detail',
                arguments: i,
              );
            },
          );
        },
      ),
    );
  }
}

// detail_page.dart
import 'package:flutter/material.dart';

class DetailPage extends StatelessWidget {
  final int itemId;
  const DetailPage({required this.itemId});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Detail $itemId')),
      body: Center(child: Text('详情项:$itemId')),
    );
  }
}

// not_found_page.dart
import 'package:flutter/material.dart';

class NotFoundPage extends StatelessWidget {
  const NotFoundPage();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('404')),
      body: const Center(child: Text('页面不存在')),
    );
  }
}

7.2.2 在 main.dart 中配置 onGenerateRoute

// main.dart (Navigator 1.0)
import 'package:flutter/material.dart';
import 'login_page.dart';
import 'home_page.dart';
import 'detail_page.dart';
import 'not_found_page.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Login-Home-Detail',
      initialRoute: '/login',
      onGenerateRoute: (settings) {
        // 登录页
        if (settings.name == '/login') {
          return MaterialPageRoute(
            builder: (_) => LoginPage(
              targetRoute: settings.arguments as String?,
            ),
          );
        }

        // Home 需要登录保护
        if (settings.name == '/home') {
          if (!isLoggedIn) {
            // 未登录,跳到登录页,并把原始目标放到 arguments
            return MaterialPageRoute(
              builder: (_) => LoginPage(targetRoute: '/home'),
            );
          }
          return MaterialPageRoute(builder: (_) => const HomePage());
        }

        // Detail 需要登录保护、同时要传参
        if (settings.name == '/detail') {
          if (!isLoggedIn) {
            return MaterialPageRoute(
              builder: (_) => LoginPage(targetRoute: '/detail'),
            );
          }
          final args = settings.arguments;
          if (args is int) {
            return MaterialPageRoute(
              builder: (_) => DetailPage(itemId: args),
            );
          }
          return MaterialPageRoute(builder: (_) => const NotFoundPage());
        }

        // 未知路由
        return MaterialPageRoute(builder: (_) => const NotFoundPage());
      },
    );
  }
}
  • 登录保护逻辑:在跳转 /home/detail 时,先检查 isLoggedIn,若为 false,则跳转登录页,并把目标路由信息放到参数 targetRoute
  • 深度链接(初次打开带参数):当用户直接通过外部调用 runApp 时指定 initialRoute(如 /detail),仍会进入 onGenerateRoute,通过相同逻辑进行登录检查与参数解析。

7.2.3 运行效果示意

  1. 用户未登录:应用启动,initialRoute='/login',直接显示登录页。
  2. 在登录页点击“登录”:将 isLoggedIn=true,跳转到 /home
  3. 在 HomePage 点击某行(item=5):执行 Navigator.pushNamed('/detail', arguments: 5),且 isLoggedIn=true,进入 DetailPage(itemId: 5)
  4. 在 DetailPage 点击返回Navigator.pop(),回到 HomePage。
  5. 用户关闭 App,再次通过 /detail/10 打开:在 runApp 时指定 initialRoute='/detail',进入 onGenerateRoute,发现 isLoggedIn=false,先跳到登录页,并把目标路由参数传给 LoginPage(targetRoute='/detail')

    • 在登录成功后,会 pushReplacementNamed('/detail'),而此时 settings.arguments 会为 null。如果想直接保留参数 10,则需要调整传递逻辑,例如将参数一起传给登录页:

      // 路径:'/detail/10'
      // 在 onGenerateRoute 里解析 uri.pathSegments
      final uri = Uri.parse(settings.name!);
      if (uri.pathSegments[0] == 'detail') {
        final id = int.tryParse(uri.pathSegments[1] ?? '');
        if (!isLoggedIn) {
          return MaterialPageRoute(
            builder: (_) => LoginPage(targetRoute: '/detail', targetArgs: id),
          );
        }
        if (id != null) {
          return MaterialPageRoute(builder: (_) => DetailPage(itemId: id));
        }
      }

7.3 进阶:Navigator 2.0 版本(Router API)

下面用 Router API 重构上面的业务逻辑,实现同样的“深度链接 + 登录保护”功能。

7.3.1 定义路由路径模型

// my_route_path.dart
abstract class MyRoutePath {}

class LoginPath extends MyRoutePath {
  final String? targetRoute;
  final int? targetItemId;
  LoginPath({this.targetRoute, this.targetItemId});
}

class HomePath extends MyRoutePath {}

class DetailPath extends MyRoutePath {
  final int itemId;
  DetailPath(this.itemId);
}

7.3.2 实现 RouteInformationParser

// my_route_parser.dart
import 'package:flutter/material.dart';
import 'my_route_path.dart';

class MyRouteParser extends RouteInformationParser<MyRoutePath> {
  @override
  Future<MyRoutePath> parseRouteInformation(
      RouteInformation routeInformation) async {
    final uri = Uri.parse(routeInformation.location ?? '/login');

    // /login
    if (uri.pathSegments.isEmpty || uri.path == '/login') {
      return LoginPath();
    }
    // /home
    if (uri.path == '/home') {
      return HomePath();
    }
    // /detail/xx
    if (uri.pathSegments.length == 2 && uri.pathSegments[0] == 'detail') {
      final id = int.tryParse(uri.pathSegments[1]);
      if (id != null) {
        return DetailPath(id);
      }
    }
    // 默认:LoginPath
    return LoginPath();
  }

  @override
  RouteInformation restoreRouteInformation(MyRoutePath configuration) {
    if (configuration is HomePath) {
      return const RouteInformation(location: '/home');
    }
    if (configuration is DetailPath) {
      return RouteInformation(location: '/detail/${configuration.itemId}');
    }
    // LoginPath
    return const RouteInformation(location: '/login');
  }
}

7.3.3 实现 RouterDelegate

// my_router_delegate.dart
import 'package:flutter/material.dart';
import 'my_route_path.dart';
import 'pages/login_page.dart';
import 'pages/home_page.dart';
import 'pages/detail_page.dart';

class MyRouterDelegate extends RouterDelegate<MyRoutePath>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<MyRoutePath> {
  @override
  final GlobalKey<NavigatorState> navigatorKey;

  bool _isLoggedIn = false;
  MyRoutePath _currentPath = LoginPath();

  MyRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();

  @override
  MyRoutePath get currentConfiguration {
    return _currentPath;
  }

  Future<void> _handleLogin({String? targetRoute, int? targetItemId}) async {
    _isLoggedIn = true;
    if (targetRoute == '/detail' && targetItemId != null) {
      _currentPath = DetailPath(targetItemId);
    } else {
      _currentPath = HomePath();
    }
    notifyListeners();
  }

  @override
  Widget build(BuildContext context) {
    List<Page> pages = [];

    // 未登录时,只显示登录页
    if (!_isLoggedIn) {
      final loginPath = _currentPath is LoginPath
          ? _currentPath as LoginPath
          : LoginPath();
      pages.add(
        MaterialPage(
          child: LoginPage(
            onLogin: () => _handleLogin(
              targetRoute: loginPath.targetRoute,
              targetItemId: loginPath.targetItemId,
            ),
          ),
        ),
      );
    } else {
      // 登录后,至少要有 HomePage
      pages.add(MaterialPage(child: HomePage(onItemTap: (id) {
        _currentPath = DetailPath(id);
        notifyListeners();
      })));

      // 如果目标是 Detail,则再压入 DetailPage
      if (_currentPath is DetailPath) {
        final itemId = (_currentPath as DetailPath).itemId;
        pages.add(MaterialPage(child: DetailPage(itemId: itemId)));
      }
    }

    return Navigator(
      key: navigatorKey,
      pages: pages,
      onPopPage: (route, result) {
        if (!route.didPop(result)) {
          return false;
        }
        // 用户点击返回键
        if (_currentPath is DetailPath) {
          _currentPath = HomePath();
          notifyListeners();
          return true;
        }
        // 如果在 Home 页面按返回,退出 App
        return false;
      },
    );
  }

  @override
  Future<void> setNewRoutePath(MyRoutePath configuration) async {
    // 当系统或浏览器传入新路由信息时触发,比如 URL 发生变化
    if (configuration is LoginPath) {
      _isLoggedIn = false;
      _currentPath = configuration;
    } else if (configuration is HomePath) {
      if (_isLoggedIn) {
        _currentPath = configuration;
      } else {
        // 未登录情况,重定向到登录,并保存目标
        _currentPath = LoginPath(targetRoute: '/home');
      }
    } else if (configuration is DetailPath) {
      if (_isLoggedIn) {
        _currentPath = configuration;
      } else {
        _currentPath = LoginPath(
            targetRoute: '/detail', targetItemId: configuration.itemId);
      }
    }
    notifyListeners();
  }
}

7.3.4 LoginPageHomePage 作相应修改

// pages/login_page.dart
import 'package:flutter/material.dart';

class LoginPage extends StatelessWidget {
  final VoidCallback onLogin;
  const LoginPage({required this.onLogin});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Login (Navigator 2.0)')),
      body: Center(
        child: ElevatedButton(
          child: const Text('登录'),
          onPressed: onLogin,
        ),
      ),
    );
  }
}

// pages/home_page.dart
import 'package:flutter/material.dart';

class HomePage extends StatelessWidget {
  final Function(int) onItemTap;
  const HomePage({required this.onItemTap});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home (Navigator 2.0)')),
      body: ListView.builder(
        itemCount: 20,
        itemBuilder: (ctx, i) {
          return ListTile(
            title: Text('Item $i'),
            onTap: () => onItemTap(i),
          );
        },
      ),
    );
  }
}

// pages/detail_page.dart
import 'package:flutter/material.dart';

class DetailPage extends StatelessWidget {
  final int itemId;
  const DetailPage({required this.itemId});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Detail (Navigator 2.0): $itemId')),
      body: Center(child: Text('详情内容:$itemId')),
    );
  }
}

7.3.5 将 Router 挂载到 App

// main.dart (Navigator 2.0)
import 'package:flutter/material.dart';
import 'my_route_parser.dart';
import 'my_router_delegate.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp();

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerDelegate: MyRouterDelegate(),
      routeInformationParser: MyRouteParser(),
      title: 'Login-Home-Detail (Navigator 2.0)',
    );
  }
}
  • 运行效果

    1. 直接在浏览器(或模拟环境)输入 http://localhost:xxxx/detail/5
    2. RouteInformationParser.parseRouteInformation 解析到 DetailPath(5),传给 setNewRoutePath
    3. setNewRoutePath 检测到用户未登录,则设置 _currentPath = LoginPath(targetRoute='/detail', targetItemId=5)
    4. build() 构建时,_isLoggedIn=false,只显示登录页;登录成功后触发 _handleLogin(targetRoute='/detail', targetItemId=5),将 _currentPath = DetailPath(5),即时 rebuild,跳转到 DetailPage(itemId=5)
    5. 当用户在 DetailPage 点击返回(Android 系统返回键),触发 popRoute()_currentPath = HomePath() → rebuild 返回 HomePage

八、总结与最佳实践

  1. Navigator 1.0 适合简单场景

    • 只需用 Navigator.push/pop 即可,代码量少,上手快。
    • 若只需移动端,并且不关心浏览器 URL 或深度链接,可优先使用。
  2. 命名路由与集中式管理

    • 当项目页面众多时,可通过 routesonGenerateRoute 把路由集中管理,提高可维护性。
    • onGenerateRoute 更加灵活,可动态解析参数并做鉴权拦截。
  3. Route 观察与页面生命周期

    • RouteObserver + RouteAware 可以监听页面进入/退出,适合做统计、业务逻辑触发。
  4. 嵌套路由与多导航栈

    • 当需要底部导航栏、侧边导航或 Tab 组合时,可用多个 Navigator 嵌套,配合 IndexedStack 保持各自状态。
    • 核心要点在于给每个子 Navigator 分配独立的 GlobalKey<NavigatorState>
  5. Navigator 2.0(Router API)适用于需要 Web 支持与深度链接的场景

    • 通过 RouteInformationParserRouterDelegate 分离“URL 解析”和“页面栈构建”两大职责,易于测试与扩展。
    • 宣告式地根据路由模型构建页面栈,更容易实现“根据状态渲染页面”的思路。
    • 学习曲线比 Navigator 1.0 更陡,但一旦掌握,可支持更复杂的路由需求。
  6. 最佳实践要点

    • 优先考虑 Navigator 1.0,在移动端小型 App 中满足大多数需求;
    • 如需深度链接,先采用 onGenerateRoute 做 URI 解析,再考虑全面切换到 Navigator 2.0;
    • 定期清理路由栈,避免过深的导航链导致内存占用和页面恢复问题;
    • 登录/鉴权 等通用规则,可在 onGenerateRoute 里实现一次性拦截,避免在每个页面中都写重复逻辑;
    • RouteObserver嵌套路由 常结合使用,解决复杂场景下的页面状态管理与事件监听。
至此,你已经从最基础的 Navigator.push / pop,到命名路由集中式管理,再到嵌套路由,乃至 Navigator 2.0 的 Router API 全面扫盲。无论是简单移动端 App,还是需要 Web URL 同步与深度链接的跨平台应用,都可以在本文范式中找到相应的最佳实践。
最后修改于:2025年06月03日 14:49

评论已关闭

推荐阅读

DDPG 模型解析,附Pytorch完整代码
2024年11月24日
DQN 模型解析,附Pytorch完整代码
2024年11月24日
AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日