15、Flow与Navigation:Navigation的Flow支持、导航事件的Flow化、Safe Args的Flow封装
Navigation 组件,说白了就是 Android 官方给咱们搭的一套页面跳转框架。但如果你用过老版本的 Navigation,你会发现它和 Kotlin Flow 之间几乎没什么交集。导航事件是回调式的,参数传递是 Bundle 式的,跟响应式编程完全是两个世界。
我个人习惯是把整个应用的状态都统一成 Flow 驱动。导航也不例外。你想想看,如果每次跳转页面都要手动调用 navController.navigate(),然后在目标页面手动解析 Bundle,那代码写起来得多痛苦?
这一章,我们就来聊聊怎么把 Navigation 和 Flow 真正地「焊」在一起。
15.1 Navigation 的 Flow 支持现状
官方在 Navigation 2.4.0 之后,其实已经加入了一些对 Kotlin 协程和 Flow 的支持。比如 currentBackStackEntry 这个属性,它本身就是一个 LiveData,但我们可以轻松地把它转成 Flow。
我在项目中遇到过这样一个场景:某个页面需要根据当前导航栈中的参数来决定 UI 的显示状态。如果用传统的 LiveData,你得手动处理生命周期。用 Flow 就简单多了。
// 将 currentBackStackEntry 转为 Flow
val backStackEntryFlow = navController.currentBackStackEntryFlow
// 在 ViewModel 中收集
viewModelScope.launch {
navController.currentBackStackEntryFlow.collect { entry ->
// 每次导航变化时,这里都会收到通知
val args = entry.arguments
// 处理参数
}
}
currentBackStackEntryFlow 是 Navigation 2.4.0+ 才有的 API。如果你还在用旧版本,建议升级。我踩过这个坑——旧版本只能用 addOnDestinationChangedListener,那玩意儿用起来真的不 Flow。
15.2 导航事件的 Flow 化
导航事件,比如「点击按钮跳转到详情页」、「登录成功后跳转到主页」,这些在传统写法里都是分散在各个 Fragment 或 Activity 中的。你想想看,如果有一天要改跳转逻辑,你得翻遍所有页面。
我的做法是:把导航事件抽象成一个 SharedFlow,在 ViewModel 中统一发射,在 Fragment 中统一收集。
// 定义导航事件
sealed class NavigationEvent {
data class GoToDetail(val id: String) : NavigationEvent()
object GoToHome : NavigationEvent()
object GoBack : NavigationEvent()
}
// 在 ViewModel 中
class MainViewModel : ViewModel() {
private val _navigationEvent = MutableSharedFlow<NavigationEvent>()
val navigationEvent: SharedFlow<NavigationEvent> = _navigationEvent.asSharedFlow()
fun onItemClicked(id: String) {
viewModelScope.launch {
_navigationEvent.emit(NavigationEvent.GoToDetail(id))
}
}
fun onLoginSuccess() {
viewModelScope.launch {
_navigationEvent.emit(NavigationEvent.GoToHome)
}
}
}
// 在 Fragment 中
lifecycleScope.launch {
viewModel.navigationEvent.collect { event ->
when (event) {
is NavigationEvent.GoToDetail -> {
findNavController().navigate(
R.id.action_to_detail,
bundleOf("id" to event.id)
)
}
NavigationEvent.GoToHome -> {
findNavController().navigate(R.id.action_to_home)
}
NavigationEvent.GoBack -> {
findNavController().popBackStack()
}
}
}
}
这样做的好处是什么?导航逻辑全部集中到了 ViewModel 中。你可以在 ViewModel 里写单元测试,模拟各种导航事件,而不需要启动 Activity 或 Fragment。
SharedFlow 时,记得设置 replay = 0。导航事件通常只需要消费一次,不需要重放。我曾经因为忘记设置,导致 Fragment 重建后重复执行了导航,页面跳了两次。
15.3 Safe Args 的 Flow 封装
Safe Args 是 Navigation 官方提供的类型安全参数传递方案。但它有个问题:参数是在 Fragment 的 onCreate 或 onViewCreated 中通过 navArgs() 获取的,这本质上是一次性的。如果你想要响应式的参数变化,就得自己封装。
我记得有一次,产品经理要求详情页的标题根据传入的 ID 动态变化。如果用 Safe Args 的传统写法,我只能拿到一次 ID,然后手动去加载数据。但如果我把 Safe Args 封装成 Flow,事情就变得优雅多了。
// 封装 Safe Args 为 Flow
inline fun <reified Args : NavArgs> Fragment.observeNavArgs(): Flow<Args> {
return callbackFlow {
// 获取当前参数
val args = navArgs<Args>().value
trySend(args)
// 监听导航栈变化,参数更新时重新发送
val listener = NavController.OnDestinationChangedListener { _, _, arguments ->
if (arguments != null) {
val newArgs = NavArgsSerializer(Args::class).fromBundle(arguments)
trySend(newArgs)
}
}
findNavController().addOnDestinationChangedListener(listener)
awaitClose {
// 清理监听器
}
}
}
// 使用示例
class DetailFragment : Fragment() {
private val args: Flow<DetailFragmentArgs> by lazy {
observeNavArgs()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch {
args.collect { safeArgs ->
// 每次参数变化时,这里都会收到新的 safeArgs
titleTextView.text = "详情: ${safeArgs.itemId}"
loadData(safeArgs.itemId)
}
}
}
}
popBackStack 返回后参数被更新),你的 UI 会自动响应。
15.4 整体架构图
下面这张图展示了 Flow 与 Navigation 结合后的整体数据流向。你可以看到,导航事件从 ViewModel 的 SharedFlow 出发,经过 Fragment 收集后调用 NavController,而 Safe Args 则通过 Flow 反向传递给 ViewModel。
15.5 避坑指南
我在实际项目中踩过不少 Navigation + Flow 的坑,这里列几个最常见的:
- 生命周期问题: 在 Fragment 中收集 Flow 时,一定要用
lifecycleScope或viewLifecycleOwner.lifecycleScope。如果用GlobalScope,Fragment 销毁后协程还在跑,轻则内存泄漏,重则崩溃。 - 重复导航: 使用
SharedFlow时,如果replay设置大于 0,Fragment 重建后会重新消费之前的导航事件,导致页面跳转两次。我的建议是导航事件一律用replay = 0。 - Safe Args 的 null 安全: 封装 Safe Args 为 Flow 时,要注意参数可能为 null。尤其是从 Deeplink 进入时,某些参数可能没传。我习惯在封装层做一次空值检查,避免下游崩溃。
- 测试难度: 导航事件 Flow 化后,单元测试变得简单了。你只需要 mock 一个
SharedFlow,然后验证 ViewModel 在特定条件下是否发射了正确的事件。不需要启动任何 UI 组件。
NavigationModule,专门放导航相关的 Flow 封装和扩展函数。这样所有页面都复用同一套逻辑,改起来也方便。你想想看,如果每个 Fragment 都自己写一遍 observeNavArgs(),那得多累。
好了,这一章的内容就到这里。Navigation 和 Flow 的结合,说白了就是把「命令式跳转」变成「响应式事件流」。你不需要在每个 Fragment 里写重复的导航代码,也不需要担心参数传递的类型安全。用 Flow 把这一切串起来,代码会干净很多。
公众号:蓝海资料掘金营,微信deep3321