跳到主要内容

Android Navigation 模块

viewcompose-navigation-android 把 viewcompose-navigation-core 状态挂载为原生 Android View 页面。 它负责目的地和图的生命周期边界、带作用域的 ViewModel Owner Lease、SavedStateRegistry 命名空间、受策略约束的子渲染会话、事务失败恢复、Android 系统返回与预测性返回、自适应内容 Pane、模态 Overlay Scene,以及感知命令类型的 View motion。

应用仍使用 Activity 或 Window 作为最外层 Android 宿主,但单个页面不需要 Activity 或 Fragment。 平台无关返回栈仍位于 viewcompose-navigation-core;本模块是它的 Android 执行边界。

产物与稳定性​

dependencies {
implementation("com.viewcompose:viewcompose-navigation-android:0.1.0-alpha02")
}
  • 稳定性:Alpha。宿主、转场和预测性返回契约在 Alpha 版本之间仍可能演进。
  • 平台:Android 库,最低 SDK 跟随仓库 Android 策略。
  • API 依赖包括 Navigation Core、Runtime、UI Contract 和 UI Foundation,因为它们的 Route、State、 Node 与 Builder 类型构成公开 Navigation API。
  • 仅运行时依赖包括 Android 集成、Activity Back 回退与 NavigationEvent 1.1.2 直接输入。 Android Renderer 仍由 Android Host 私有传递,navigationevent-testing 仍只用于测试。
  • 该产物会传递引入 viewcompose-navigation-core;只需要平台无关模型时可单独依赖 core。

Controller 与宿主​

fun UiTreeBuilder.AppNavigation() {
val controller = rememberNavHostController(
startDestination = NavRoute("home"),
)
NavHost(controller = controller) { entry ->
when (entry.route.name) {
"home" -> HomePage(controller)
"details" -> DetailsPage(controller)
else -> error("Unknown route ${entry.route.name}")
}
}
}

自定义 Overlay Transport 属于构造输入。普通渲染之间应保持 Factory 引用稳定,仅在必须重建 Transport 时推进显式 Key:

fun UiTreeBuilder.customOverlayNavHostSample(
controller: NavHostController,
overlayHostFactory: (ViewGroup) -> OverlayHost,
overlayFactoryVersion: Any,
) {
NavHost(
controller = controller,
overlayHostFactory = overlayHostFactory,
key = overlayFactoryVersion,
) { entry ->
Text(entry.route.name)
}
}

一个 NavHostController 同时只能连接一个活跃 NavHost。导航命令必须在主线程调用且要求宿主 已连接,确保 core 事务、目的地渲染、owner 生命周期和原生 View 层级共用同一个提交边界。

类型化命令​

data class ArticleRoute(val articleId: Long)

fun typedRouteNavigationSample(
controller: NavHostController,
destination: NavRouteSpec<ArticleRoute>,
): ArticleRoute {
controller.navigate(destination, ArticleRoute(articleId = 42L))
return controller.snapshot.top.toRoute(destination)
}

Graph 声明、navigate、replaceTop、reset 与 NavEntry.toRoute 共用同一个 NavRouteSpec<T>。编码会在主线程且 Host 事务开始前完成,因此 Encoder 异常不会改变 Stack、 Render Tree、Owner Lifecycle 或 Result Inbox。Controller 和 Saved-state Adapter 仍只接收 NavRoute,不会保留存活 Route 对象或 Callback。

NavHost 分离保留逻辑 Owner 与原生 Presentation。Scene 发布前,会用最新环境重建缺失的可见 Presentation;失败则释放候选并保留已提交 Stack。不可观察内容输入变化时修改 contentKey; Ownership 输入或 Overlay Factory 变化时修改 Host key。默认 Overlay Factory 显式使用 viewcompose-overlay-android,不会依赖 Classpath 发现。

Destination Context 上下文​

只有在声明最近 Destination 的内容时,LocalNavDestinationContext.current 才非空。稳定的 NavDestinationContext 会公开精确的 NavEntry 身份和只读 State<NavDestinationPresentation>。NavDestinationPresentation 是 Navigation Core NavSceneEntry 的源码别名,因此 Visibility、Interaction、Transition Phase、Pane Role 和 Content/Overlay Layer Role 不会与 Lifecycle 规划所用 Scene 分叉。

fun UiTreeBuilder.destinationContextSample(controller: NavHostController) {
NavHost(controller = controller) { entry ->
val presentation = checkNotNull(LocalNavDestinationContext.current).presentation.value
Text("${entry.route.name}: ${presentation.visibility}, ${presentation.paneRole}")
}
}

后续回调需要 Destination 身份时,应在 DSL 声明阶段捕获 Context。同一 Retained Entry 释放 Presentation 后仍保留它,永久移除后则停止更新。资源阈值使用 AndroidX Lifecycle;Presentation 仅用于粗粒度可见性、Pane 与 Transition UI。嵌套 Host 提供最近 Context,不存在全局 Current Page 查询。

模态导航 Scene​

向 NavHost 传入稳定且有序的 sceneStrategies,把末尾 Destination 投影到普通内容 Pane 之上; 首个返回 Layout 的 Strategy 生效:

val ModalDestinationStrategy = NavSceneStrategies.trailingOverlays { entry ->
entry.route.name.endsWith("-dialog")
}

fun UiTreeBuilder.overlaySceneNavHostSample(controller: NavHostController) {
NavHost(
controller = controller,
sceneStrategies = listOf(ModalDestinationStrategy),
) { entry ->
Text(entry.route.name)
}
}

Covered Layer 保持可见且处于 STARTED;只有顶部 Overlay 拥有 Input、Accessibility 与 RESUMED。透明全宿主 Container 会阻止 Pointer、Generic-motion 与 Key 穿透;Destination 内容 绘制 Surface/Scrim。Overlay 复用内容的 Owner、Result、Restore、Back 与 Cleanup。模态 Motion 只移动 Overlay,并禁止跨 Layer Shared Matching。Layout 必须保留内容并仅分类精确末尾后缀。

向上一页返回结果​

val SelectedItemResult = NavResultKey.text("catalog.selection")

fun UiTreeBuilder.observeSelectedItem(onSelected: (String) -> Unit) {
NavResultEffect(SelectedItemResult, onSelected)
}

fun returnSelectedItem(controller: NavHostController, itemId: String): NavResult =
controller.popBackStack(SelectedItemResult, itemId)

已提交 Pop 会把值写入仍存活 Entry 的可保存 FIFO Inbox。NavResultEffect 在 Destination 到达 RESUMED 后至多消费一次;显式确认或重试应使用 NavDestinationContext.results。Key 仅属于 本地 Entry,不是全局或跨栈总线。

展示保留策略​

NavPresentationRetentionPolicy 独立于 Entry Ownership 控制原生展示生命周期。 DisposeWhenHidden 是默认策略:转场稳定后,每个完全隐藏页面的子 RenderSession 和 View Tree 都会释放,而 Entry Owner 仍保留在 CREATED。RetainAll 是显式的无界选择,只应在实测证明 Surface 重建代价足以抵消内存、Effect、Focus、Accessibility 与原生资源成本时使用。Bounded 保留正数上限的隐藏展示,并按确定性的“最久未隐藏”顺序淘汰。可见 Scene Entry、普通转场参与者和预测性 转场参与者都不计入该上限。

fun UiTreeBuilder.BoundedPresentationNavigation(controller: NavHostController) {
NavHost(
controller = controller,
presentationRetentionPolicy = NavPresentationRetentionPolicy.Bounded(
maxHiddenPresentations = 2,
),
) { entry ->
Text(entry.route.name)
}
}

修改现有 Host 的策略不会重建 Host 或任何 Entry Owner。收紧上限会立即释放超限的隐藏展示;放宽 策略只影响之后创建或隐藏的展示,不会急切构建当前不可见页面。首次连接、配置恢复连接和进程恢复 连接都只物化当前 Scene Layout,即使选择 RetainAll 也是如此。

Retention 权衡与证据解释由导航架构维护。

命令结果与重入​

Controller 命令返回 NavResult:

  • Committed 报告宿主已应用的状态和 entry owner 差异;
  • NoChange 报告合法但已经生效的命令;
  • Queued 表示转场或回调正在执行,命令稍后串行运行;
  • Failed 报告结构化渲染或提交上下文。

目的地回调可能在另一轮渲染、生命周期更新或 motion 完成期间同步导航。宿主会把重入命令排队, 只在当前操作达到终态后继续执行。因此 queued 结果不代表完成;应观察 controller.navigationState 获取最终提交的多栈状态。

Controller 提供即时不可变 snapshot 和 stackState,以及可观察 navigationState。Tab 选中 状态应从 activeStackId 派生,不要维护第二份状态源。

类型化 Plan 执行​

Navigation Core Reducer 是 Lifecycle、Retention、Input、Accessibility 与 Back 策略的唯一来源。 Android Executor 在 Commit 前准备 Presentation,随后发布 Plan 指定的 Scene 与有序 Effect; Rollback 和终态清理使用 Plan ID,而不检查 View。应用通常只使用 NavHost;Reducer 是测试和 自定义 Executor 的 Q3 边界。

目的地与图 Ownership​

每个 Destination Entry 独立拥有 Lifecycle、ViewModelStore、SavedStateRegistry、 SavedStateHandle 默认参数和 Saveable State;Graph 实例为其后代持有同类 Scope。隐藏保留会维持 这些身份并限制 Lifecycle。Transition 参与者最高为 STARTED,已 Pop Entry 在 Presentation 释放前为 CREATED,只有永久移除才进入 DESTROYED;重复 Route 仍创建不同 Owner。

NavHost 要求最近的 LocalViewModelStoreOwner 并继承默认 Factory 与 CreationExtras;底层 renderInto 调用者必须显式提供。持久化 Host-scope ID 允许配置重建继续租用相同 Entry/Graph Store,永久移除则清理它们。在 Destination 内容内使用 ProvideNavGraphOwner(route) 选择活跃 Graph Scope。内容 Pane 与模态 Overlay 使用同一组 Owner 和 Lifecycle 阈值;独立的 UI Overlay Transport 只用于承载临时 Motion Snapshot。

失败与回滚​

Android 宿主保持 navigation core 的两阶段保证:先准备新目的地会话和 owner,再 stage 到 View 层级,然后提交纯栈状态,最后运行提交副作用。失败由 NavFailurePhase 分类。

NavFailure.stackCommitted 区分不可逆栈边界前后的失败。提交前失败会移除候选会话和 owner, 并回滚 core 事务;提交后失败保留已提交状态并报告副作用问题,不会假装旧栈仍是事实来源。

保留页面在显示前刷新失败时,会以 DestinationRefresh 和 stackCommitted = false 报告。 此前的 Stack、Scene Layout、可见 View、Owner 与会话继续有效,预测性返回 Preview 或 Pane 扩展 不会发布。

可向 NavHost 传入 onFailure 处理日志、降级或测试。未处理失败会抛出 NavHostException, 其中保留原始 cause、失败 entry 和 renderer frame report。

保存、恢复与进程死亡​

Saveable Registry 会持久化 Stack、历史、Entry/Graph ID 与 Route、Owner Bundle 和 Saveable 值,不会序列化待处理工作、View、Session、Lifecycle 对象或 ViewModel 内容。恢复连接只物化当前 Content-and-overlay Scene;Scene Strategy 会针对恢复后的 Stack 和当前宽度重新运行。版本、结构、 上限、配置或 Graph 层级无效时会 Fail Closed 到初始状态;相邻 Version 4 格式 会以新的 Host-scope ID 恢复。

Android 系统返回与预测性返回​

处于 STARTED 且可 Pop 时,NavHost 向最近的 ViewTreeNavigationEventDispatcherOwner 安装一个 Handler,仅在无该 Owner 时回退 Activity Back;根节点禁用 Handler 并向外委派。两条互斥路径共用 事务式 Preview/Pop 状态机:取消恢复 Scene,提交只 Pop 一次;Stop、Detach、禁用、Owner 变化或销毁 均先取消再注销,并抑制已取消手势迟到的终态。尚无 Forward History 与 Android Studio Preview 输入。

Motion 动效​

NavTransitionSpec 是覆盖所有命令和 Predictive Back 的纯视觉策略。 NavDestinationTransform 组合 Pane/dp 位移、Alpha、Scale、Timing 与 Easing;None 禁用 Motion。 Driver 会先布局端点、为 Transform 临时使用硬件 Layer,并从当前视觉属性重定向,不改变 Stack 或 Owner 语义。

共享内容动效​

sharedElement 与 sharedBounds 是在单个 Destination Pair 内按 Key 与 Mode 唯一匹配的 Q3 标记。 无效、Detach、Surface-backed 或超预算端点按 Key 回退,不影响导航。单 Window 实现会在不可交互 Overlay 中执行有界 Snapshot 动画,保持 Incoming Input/Accessibility Ownership,且只清理一次; Predictive Back 驱动同一视觉层,但不会获得 Commit 权限。模态转场跳过 Shared Matching,避免 Snapshot 跨越 Overlay 边界。

自适应 pane​

NavPanePolicy.Single 在所有宽度下保持单页面全屏宿主。Adaptive 会在每个 pane 都满足最小宽度 时展示最多三个最新 entry。决定 pane 数之前会扣除 paneSpacingDp。

宽度变化会复用已提交返回栈、目的地会话和 Owner,只在重新计算原生 Child Bounds 前刷新新进入 Content Scene 的保留 Entry。Scene Strategy 在 Pane 选择前运行,因此 Overlay 分类跨宽度保持稳定, 只有内容前缀重新流式布局。布局方向会把 Primary 到 Tertiary 映射为 LTR 或 RTL 下正确的物理顺序。

深链与保留栈​

fun navigateSharedImageRequest(controller: NavHostController): NavDeepLinkResult {
return controller.navigateDeepLink(
NavDeepLinkRequest(
action = Intent.ACTION_SEND,
mimeType = "image/png",
),
)
}

fun navigateSharedImageIntent(
controller: NavHostController,
intent: Intent,
): NavDeepLinkResult {
return controller.navigateDeepLink(intent)
}

Request、URI 与 Intent 入口共用严格 Core Resolver;Intent 只映射 data、action 和 type。 Match 会原子更新并选中目标 Stack,嵌套 NavResult 仍保留 Render/Commit 失败。多 Tab 应共享一个 Remembered Controller 和 NavStackConfiguration,不要镜像 Active-stack State。

相关文档​

完整生成参考位于 viewcompose-navigation-android API 树。

兼容性说明​

0.1.0-alpha01 确立了一个 controller 对一个 host 的连接、主线程串行命令、目的地和图 ownership、 防御式进程死亡恢复、预测性返回 preview、对齐 Android 的原生 View motion,以及最多三个自适应 pane。请通过 rememberNavHostController 持久化 controller 状态,不要在宿主之外保留 Android owner 或 session 对象。

Lifecycle 2.11 硬切要求 NavHost 位于 LocalViewModelStoreOwner 下。现有 Activity 与 Fragment setUiContent 集成已满足要求;自定义 renderInto Host 必须补充 ProvideViewModelStoreOwner,不会保留隐式 Root Store 或兼容别名。

类型化 Shared-content Marker 是新增 Q3 UI Contract API,但只有发布稳定端点 Tag 的 Renderer 与 本 Navigation Host 实现组合时才产生动效;旧版或自定义 Renderer 可以把 Marker 视为无效。 跨 Window、跨 Activity、跨 Process、Live Content、Shape Morph 与任意 Surface-backed Capture 在本 Alpha 中有意不支持,并回退到普通 Destination Motion。

本 Alpha 在 presentationRetentionPolicy 前插入 sceneStrategies;该位置的位置调用须迁移为 命名参数。重组时应保持 Strategy 实例稳定。