Deferred Navigation 是一种延迟导航模式,其中到下一个屏幕的转换在异步操作完成后发生,而不是在用户操作时直接发生。根据 Android Developers (2024),延迟导航可以避免导航和数据加载之间的竞态条件,同时简化来自推送通知和 Deeplink 的转换处理。关键区别 — 路由在所有必要数据可用后计算。
要点
Deferred Navigation 是一种架构模式,其中导航决策被延迟到所有必要数据可用时。与直接转换不同,用户点击按钮后立即进入新屏幕,延迟导航将触发事件和实际转换分开,在它们之间添加异步操作。
在架构上,Deferred Navigation 基于状态变化:按钮点击启动异步过程,对其结果的订阅触发导航。这在具有 MVVM 或 MVI 架构的应用程序中尤为重要,其中 ViewModel 管理状态,View(Activity、Fragment、SwiftUI View)订阅变化并通过转换做出响应。这种方法消除了 UI 和导航逻辑之间的直接依赖关系。
根据 Google I/O 2023 的数据,延迟导航推荐用于所有导航依赖于网络请求结果、授权检查、配置加载或权限的场景。在处理 Deeplink 时,此模式也是强制性的,当应用程序必须先启动、加载根屏幕然后才能导航到目标路由。
Deferred Navigation 应用于直接转换导致屏幕状态错误或加载错误的场景。让我们考虑延迟导航是强制性的四种主要情况。
如果用户点击受保护的内容,应用程序必须首先检查访问令牌。直接导航到内容屏幕将导致空白屏幕或 401 错误(如果令牌已过期)。延迟导航检查令牌,仅在成功时进入目标屏幕。失败时重定向到登录屏幕。
当应用程序通过外部链接打开时,必须先加载根屏幕、恢复导航状态,然后才能通过 Deeplink 执行转换。没有根上下文的直接导航到目标屏幕将导致异常:导航堆栈为空或 back stack 损坏。
点击推送通知时,应用程序可能处于三种状态:关闭、后台或活动。Deferred Navigation 确定应用程序状态,加载必要内容,然后才显示目标屏幕。iOS 允许通过 UNNotificationContentExtension 处理此场景。
如果屏幕功能由服务器端的功能开关控制,延迟导航允许先请求配置然后再显示屏幕。如果功能被禁用,用户看到替代内容或占位符而不是空白屏幕。
| 场景 | 直接导航 | Deferred Navigation |
|---|---|---|
| 授权 | 令牌过期时空白屏幕 | 重定向到登录 |
| Deeplink | Back stack 损坏 | 正确的导航堆栈 |
| 推送 | 无上下文加载 | 数据在转换前准备好 |
| 功能开关 | 显示不可用功能 | 占位符或替代方案 |
直接导航 是一种传统方法,其中转换作为对事件的响应立即执行。用户点击按钮,UI 路由器立即切换屏幕。这种方法简单且可预测,但在需要服务器数据或条件检查的场景中受到限制。
Deferred Navigation 添加了一个异步状态的层次。用户事件启动操作,对结果的订阅管理导航。这增加了代码的复杂性,但提供了灵活性:相同的触发器可以根据加载的数据导向不同的屏幕。
在两种方法之间的选择取决于需求:如果显示屏幕不需要异步数据 — 使用直接导航。如果屏幕依赖于请求结果、授权或外部条件 — 延迟导航是强制性的。混合方法(其中部分转换是直接的,部分是延迟的)是生产应用程序中最常见的实践。
Android Jetpack 提供了在架构级别实现 Deferred Navigation 的机制。主要思想 — ViewModel 管理状态,Activity 或 Fragment 订阅变化并通过 NavController 触发导航。
Kotlin 协程中的 StateFlow 是延迟导航的理想工具。ViewModel 使用导航事件更新 StateFlow,Activity 观察它并执行转换。事件处理后,StateFlow 被清除,防止重复导航。
class MainViewModel : ViewModel() {
private val _navigation = MutableSharedFlow<NavigationEvent>()
val navigation: SharedFlow<NavigationEvent> = _navigation
fun onDeepLinkReceived(link: String) {
viewModelScope.launch {
val data = resolveDeepLink(link)
_navigation.emit(NavigationEvent.GoToScreen(data))
}
}
}
在 Activity 中,订阅导航触发带有 ViewModel 路由的 NavController。为防止屏幕旋转时重复导航,使用 NavigationEventWrapper 包装器,它只处理一次事件。Jetpack Navigation 2.7+ 支持 Safe Args 用于类型安全的参数传递。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val vm: MainViewModel by viewModels()
repeatOnLifecycle(Lifecycle.State.STARTED) {
vm.navigation.collect { event ->
when (event) {
is NavigationEvent.GoToScreen ->
findNavController(R.id.nav_host)
.navigate(event.route)
}
}
}
}
}
iOS 没有类似于 Android Jetpack 的内置 Navigation Component,因此开发人员通过 Coordinator Pattern 结合 Combine 或 async/await 实现 Deferred Navigation。Coordinator 管理屏幕堆栈并根据加载的数据做出导航决策。
Coordinator 是一个管理 ViewController 之间导航的对象。与 Combine 配合,ViewModel 通过 PassthroughSubject 发布事件,Coordinator 订阅它们并执行转换。这种方法将 UI 与导航逻辑完全分离,并符合 Apple 的应用程序架构建议。
final class AppCoordinator {
private var cancellables = Set<AnyCancellable>()
func start(viewModel: MainViewModel) {
viewModel.$navigationDestination
.compactMap { $0 }
.sink { [weak self] destination in
self?.navigateTo(destination)
}
.store(in: &cancellables)
}
}
Swift 5.5 引入了结构化并发,允许通过 async/await 实现延迟导航而无需 Combine。ViewModel 提供一个异步函数,在数据加载后返回路由。Coordinator 在 Task 中调用此函数并根据接收到的路由执行转换。
class AuthViewModel: ObservableObject {
func resolveDeeplink(_ url: URL) async -> AppRoute? {
guard let token = await AuthService.shared.getValidToken() else { return .login }
return await DeeplinkRouter.resolve(url, token: token)
}
}
// 在 Coordinator 中:
Task {
if let route = await viewModel.resolveDeeplink(url) {
navigateTo(route)
}
}
Deferred Navigation 简化了异步场景的处理,但需要对状态管理采取纪律性的方法。让我们看看开发人员在实现延迟导航时犯的主要错误。
最常见的错误 — 在根屏幕完全初始化且 NavController 或 Coordinator 准备好转换之前尝试执行 延迟导航。在 Android 中这导致 IllegalStateException,在 iOS 中 — 未定义的 UI 状态。解决方案 — 在启动导航之前确保组件的生命周期处于 STARTED 或 RESUMED 状态。
如果 StateFlow 或 Subject 在处理后不清除事件,返回上一个屏幕时用户可能被自动重定向到同一屏幕。在 Android 中使用 SharedFlow(replay=0)或在 iOS 中使用处理后设置为 nil 的 CurrentValueSubject,以便导航事件只触发一次。
使用一个中央组件管理应用程序中的整个导航。当每个 Activity、Fragment 或 ViewController 都有自己的导航控制器时,应用程序不同部分之间的延迟导航变得混乱。单一的 Coordinator 便于调试和测试导航场景。
延迟导航比直接导航更难测试,因为异步操作引入了时间因素。在 Android 中使用 TestDispatcher(kotlinx-coroutines-test)和在 iOS 中使用 XCTestExpectation 来模拟数据加载,并验证导航是否按预期路由执行。模拟授权和深度链接服务以隔离测试每个场景。
常见问题
Deferred Navigation 是一种延迟转换模式,可用于任何异步场景。Deep Link 是延迟导航的触发器之一,但不是唯一的。授权和功能开关也使用延迟导航。
可以,大多数应用程序使用混合方法。产品列表屏幕(没有异步依赖)可以使用直接导航,而带有数据加载的详情屏幕使用延迟。划分由特定屏幕的架构决定。
使用 Android 中不带 replay 的 SharedFlow 和 iOS 中不带缓冲的 combineLatest。在新触发器时取消先前的订阅。这确保只处理最后一个导航事件。
支持,在 Compose 中延迟导航通过订阅 ViewModel 的 StateFlow 并在 LaunchedEffect 中调用 NavController.navigate 来实现。Google 推荐将 Navigation Compose 与事件模型一起用于延迟场景。
延迟导航直到应用再次变为活动状态。在 Android 中使用 Lifecycle.State.STARTED 过滤事件。在 iOS 中检查 Combine 或 async/await 块中的 UIApplication.State。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。