深色模式
Navigator
一个以栈式规则管理一组子组件的组件。
许多应用会在组件层级的顶部附近放置一个导航器(navigator),通过[Overlay](覆盖层)展示逻辑导航历史——最近访问的页面会以视觉形式显示在旧页面之上。采用这种模式后,导航器可通过在覆盖层中移动组件,实现页面间的视觉过渡效果。类似地,导航器也可通过将对话框组件置于当前页面之上,实现对话框的显示。
使用 Pages API
若为[Navigator]提供了[Navigator.pages]参数,它会将该参数转换为一个[Route](路由)栈。当[Navigator.pages]发生变化时,会触发路由栈的更新,导航器会调整其路由以匹配[Navigator.pages]的新配置。
使用该 API 时,需创建[Page]的子类,并为[Navigator.pages]定义一个[Page]列表。同时,必须提供[Navigator.onPopPage]回调——当发生页面弹出(pop)操作时,该回调会正确清理输入的页面列表。
默认情况下,[Navigator]会使用[DefaultTransitionDelegate](默认过渡代理)决定路由进入或退出屏幕的过渡方式。若需自定义过渡效果,可创建[TransitionDelegate]的子类,并将其传入[Navigator.transitionDelegate]参数。
有关 Pages API 的更多用法,可参考[Router]组件。
使用 Navigator API
移动应用通常通过名为“屏幕”或“页面”的全屏元素展示内容。在 Flutter 中,这类元素被称为路由(Route),由[Navigator]组件管理。导航器会维护一个[Route]对象栈,并提供两种栈管理方式:声明式 API([Navigator.pages])和命令式 API([Navigator.push]与[Navigator.pop])。
当你的用户界面符合“栈”的范式(即用户需返回栈中更早的元素)时,使用路由和导航器是合适的。在部分平台(如 Android)中,系统 UI 会提供一个返回按钮(位于应用边界之外),用户可通过该按钮返回应用栈中更早的路由;而在没有内置导航机制的平台上,使用[AppBar](通常用于[Scaffold.appBar]属性)可自动添加返回按钮,供用户导航。
显示全屏路由
虽然可直接创建导航器,但最常见的用法是使用由Router
创建的导航器——该Router
由[WidgetsApp]或[MaterialApp]组件创建并配置。可通过[Navigator.of]方法获取该导航器。
[MaterialApp]是最简单的配置方式:其home
属性会成为[Navigator]栈底部的路由,即应用启动时显示的内容。
dart
void main() {
runApp(const MaterialApp(home: MyAppHome()));
}
若要在栈中推入新路由,可创建[MaterialPageRoute]实例,并通过构建器函数(builder)定义屏幕上需显示的内容。示例如下:
dart
Navigator.push(context, MaterialPageRoute<void>(
builder: (BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('My Page')),
body: Center(
child: TextButton(
child: const Text('POP'),
onPressed: () {
Navigator.pop(context); // 弹出当前路由
},
),
),
);
},
));
路由通过构建器函数(而非子组件)定义其组件,原因是路由会根据推入(push)和弹出(pop)的时机,在不同的上下文(context)中多次构建和重建。
如上述代码所示,调用导航器的pop
方法可弹出新路由,重新显示应用的主页:
dart
Navigator.pop(context);
对于包含[Scaffold]的路由,通常无需手动提供“弹出导航器”的组件——因为 Scaffold 会自动在其 AppBar 中添加“返回”按钮,点击该按钮会触发[Navigator.pop]。在 Android 上,按下系统返回按钮也会执行相同操作。
使用命名导航路由
移动应用常需管理大量路由,通过名称引用路由是最便捷的方式。按照惯例,路由名称采用类路径格式(例如/a/b/c
),应用主页路由的默认名称为/
。
创建[MaterialApp]时,可传入一个Map<String, WidgetBuilder>
参数——该映射将路由名称与创建路由的构建器函数关联。[MaterialApp]会使用此映射为导航器的onGenerateRoute
回调生成值。
dart
void main() {
runApp(MaterialApp(
home: const MyAppHome(), // 成为名称为 '/' 的路由
routes: <String, WidgetBuilder> {
'/a': (BuildContext context) => const MyPage(title: Text('page A')),
'/b': (BuildContext context) => const MyPage(title: Text('page B')),
'/c': (BuildContext context) => const MyPage(title: Text('page C')),
},
));
}
通过名称显示路由的方法如下:
dart
Navigator.pushNamed(context, '/b'); // 推入名称为 '/b' 的路由
路由可返回值
当推入路由以向用户请求值时,可通过pop
方法的result
参数返回该值。
推入路由的方法会返回一个[Future]对象:当路由被弹出时,该 Future 会完成(resolve),其值即为pop
方法的result
参数。
例如,若需让用户点击“OK”确认某个操作,可通过await
等待[Navigator.push]的结果:
dart
bool? value = await Navigator.push(context, MaterialPageRoute<bool>(
builder: (BuildContext context) {
return Center(
child: GestureDetector(
child: const Text('OK'),
onTap: () { Navigator.pop(context, true); } // 返回值为 true
),
);
}
));
- 若用户点击“OK”,
value
的值为true
; - 若用户退出路由(例如点击 Scaffold 的返回按钮),
value
的值为null
。
当路由用于返回值时,路由的类型参数必须与pop
的结果类型匹配。因此,上述示例中使用MaterialPageRoute<bool>
,而非MaterialPageRoute<void>
或仅MaterialPageRoute
(若无需指定类型,也可省略)。
弹窗路由(Popup routes)
路由并非必须覆盖整个屏幕。[PopupRoute](弹窗路由)会使用[ModalRoute.barrierColor](遮罩颜色)覆盖屏幕,该颜色可设置为半透明,以显示下方的当前屏幕。弹窗路由是“模态”的,因为它会阻止用户与下方组件交互。
Flutter 提供了创建并显示弹窗路由的函数,例如:[showDialog](显示对话框)、[showMenu](显示菜单)、[showModalBottomSheet](显示底部弹窗)。这些函数会返回所推入路由的 Future(如前文所述),调用者可通过await
等待路由弹出,或获取路由返回的值。
此外,部分组件也会创建弹窗路由,例如[PopupMenuButton](弹窗菜单按钮)和[DropdownButton](下拉按钮)。这些组件会内部创建[PopupRoute]的子类,并通过导航器的push
和pop
方法显示/关闭弹窗。
自定义路由
你可以创建[PopupRoute]、[ModalRoute]或[PageRoute]等组件库路由类的子类,以控制路由的动画过渡效果、模态遮罩的颜色与行为,以及路由的其他特性。
[PageRouteBuilder]类允许通过回调定义自定义路由。以下示例展示了一个“旋转+淡入淡出”的路由——路由显示/消失时,其子组件会同时执行旋转和淡入淡出动画。该路由不会覆盖整个屏幕(因为它指定了opaque: false
,与弹窗路由一致):
dart
Navigator.push(context, PageRouteBuilder<void>(
opaque: false, // 不透明:false(半透明)
pageBuilder: (BuildContext context, _, _) {
return const Center(child: Text('My PageRoute')); // 路由内容
},
transitionsBuilder: (_, Animation<double> animation, _, Widget child) {
// 过渡动画:淡入淡出 + 旋转
return FadeTransition(
opacity: animation, // 淡入淡出动画
child: RotationTransition(
turns: Tween<double>(begin: 0.5, end: 1.0).animate(animation), // 旋转动画(从半圈到一圈)
child: child,
),
);
}
));
页面路由的构建分为两部分:
- “页面(page)”:即
pageBuilder
返回的内容,通常仅构建一次(因其不依赖动画参数,示例中用_
省略); - “过渡(transitions)”:即
transitionsBuilder
定义的动画,在动画持续期间会每帧重建一次。
(本示例中,路由的返回类型为void
,表示该路由不返回值。)
嵌套导航器(Nesting Navigators)
一个应用可使用多个[Navigator]。将一个[Navigator]嵌套在另一个[Navigator]之下,可用于创建“内部导航流程”——例如标签页导航(tabbed navigation)、用户注册流程、商店结账流程,或其他代表应用子模块的独立导航流程。
示例
iOS 应用的标签页导航是典型场景:每个标签页维护自己的导航历史,因此每个标签页都有独立的[Navigator],形成“并行导航”。
除了标签页的并行导航,应用仍可启动覆盖整个标签页的全屏页面(例如引导流程、警告对话框)。因此,必须存在一个位于标签页导航之上的“根导航器(root Navigator)”——标签页的各个[Navigator]实际上是嵌套在根导航器之下的子导航器。
在实际开发中,标签页导航的嵌套[Navigator]通常由[WidgetsApp]和[CupertinoTabView]组件管理,无需手动创建或维护。
{@tool sample} 以下示例展示了如何使用嵌套[Navigator]实现独立的用户注册流程。
尽管本示例使用两个[Navigator]演示嵌套,但仅用一个[Navigator]也可实现类似效果。
运行此示例时,需执行命令flutter run --route=/signup
,使应用启动时直接进入注册流程(而非主页)。
代码详见:examples/api/lib/widgets/navigator/navigator.0.dart
[Navigator.of]方法会从给定的[BuildContext]中查找最近的祖先[Navigator]。在包含嵌套[Navigator]的大型build
方法中,需确保传入的[BuildContext]位于目标[Navigator]之下。可使用[Builder]组件在组件子树的指定位置获取[BuildContext]。
获取包含当前组件的路由
在模态路由(modal route)的常见场景中,可在构建方法(build method)内部通过[ModalRoute.of]获取包含当前组件的路由。
若需判断包含当前组件的路由是否为“活跃路由”(例如,当路由不活跃时淡化控件),可检查返回路由的[Route.isCurrent]属性。
状态恢复(State Restoration)
若为[Navigator]提供了restorationScopeId
,且其被有效的[RestorationScope](恢复作用域)包裹,则导航器会在状态恢复时:
- 重建当前的[Route]历史栈;
- 恢复这些[Route]的内部状态。
但并非栈中的所有[Route]都能恢复,具体规则如下:
- 基于[Page]的路由:若提供了[Page.restorationId],则可恢复状态;
- 通过传统命令式 API([push]、[pushNamed]等)添加的路由:永远无法恢复状态;
- 通过可恢复命令式 API([restorablePush]、[restorablePushNamed]等名称含“restorable”的命令式方法)添加的路由:
- 若其下方(直至并包含第一个基于[Page]的路由)的所有路由均可恢复,则该路由可恢复;
- 若其下方无基于[Page]的路由,则需下方所有路由均可恢复,该路由才能恢复。
若某个[Route]被判定为“可恢复”,导航器会为其[Route.restorationScopeId]设置非空值。路由可使用该 ID 存储和恢复自身状态。例如,[ModalRoute]会使用此 ID 为其内容组件创建[RestorationScope]。