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 的 onCreateonViewCreated 中通过 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)
            }
        }
    }
}
核心思路: 把 Safe Args 从「一次性获取」变成「响应式流」。这样当导航栈中的参数发生变化时(比如通过 popBackStack 返回后参数被更新),你的 UI 会自动响应。

15.4 整体架构图

下面这张图展示了 Flow 与 Navigation 结合后的整体数据流向。你可以看到,导航事件从 ViewModel 的 SharedFlow 出发,经过 Fragment 收集后调用 NavController,而 Safe Args 则通过 Flow 反向传递给 ViewModel。

Flow + Navigation 架构图 ViewModel SharedFlow<NavigationEvent> StateFlow<UiState> Flow<Args> (封装后) Fragment 收集导航事件 调用 NavController observeNavArgs() Navigation NavController NavHost Safe Args collect navigate() args Flow args 回调 数据流向说明: 1. ViewModel 通过 SharedFlow 发射导航事件(如:GoToDetail) 2. Fragment 收集事件,调用 NavController.navigate() 3. Navigation 通过 Safe Args 传递参数给目标 Fragment 4. Fragment 将 Safe Args 封装为 Flow,传递给 ViewModel 5. ViewModel 根据参数加载数据,更新 UI State 整个流程是单向的、响应式的,便于测试和维护

15.5 避坑指南

我在实际项目中踩过不少 Navigation + Flow 的坑,这里列几个最常见的:

  • 生命周期问题: 在 Fragment 中收集 Flow 时,一定要用 lifecycleScopeviewLifecycleOwner.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