Jetpack Compose中viewModel中路由跳转

前言

在 Jetpack Compose 中,有时我们定义了一些全局的事件是在ViewModel中监听的,有时需要进行路由跳转。

但是ViewModel 本身不应该直接控制页面跳转(Navigation),因为这会违反 关注点分离(Separation of Concerns) 原则:

  • ViewModel 负责业务逻辑和状态管理;
  • UI(Composable) 负责展示和导航。

但你可以通过 “事件驱动” 的方式,让 ViewModel 发出导航事件,由 UI 层监听并执行跳转。

推荐做法:

使用 NavigationEventSharedFlow / StateFlow

方案一(推荐)

使用 MutableSharedFlow 发送一次性导航事件

ViewModel中发送事件

1
2
3
4
5
6
7
8
9
10
11
class MyViewModel : ViewModel() {
private val _navigateToDetail = MutableSharedFlow<String>()
val navigateToDetail = _navigateToDetail.asSharedFlow()

fun onItemClicked(url: String) {
// 执行业务逻辑...
viewModelScope.launch {
_navigateToDetail.emit(url) // 发出导航事件
}
}
}

使用 SharedFlow 是因为它适合一次性事件(如导航、Toast),不会被重复消费。

Composable中监听

1
2
3
4
5
6
7
8
9
10
11
@Composable
fun HomeScreen(
vm: MyViewModel = hiltViewModel(),
navController: NavController
) {
LaunchedEffect(Unit) {
vm.navigateToDetail.collect { url ->
navController.navigate(url)
}
}
}

注意:

LaunchedEffect 保证只收集一次,避免重复订阅。

方案二

使用封装的 NavigationEvent 类(更安全)

为避免事件重复消费或内存泄漏,可封装一个 Event 包装类(类似 Android 的 SingleLiveEvent):

1
2
3
4
5
6
7
8
9
10
11
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set

fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) null else {
hasBeenHandled = true
content
}
}
}

然后在 ViewModel 中:

1
2
3
4
5
6
private val _navigationEvent = MutableStateFlow<Event<String>?>(null)
val navigationEvent: StateFlow<Event<String>?> = _navigationEvent

fun navigateToDetail(id: String) {
_navigationEvent.value = Event("detail/$id")
}

在 Composable 中:

1
2
3
4
5
6
7
LaunchedEffect(vm.navigationEvent) {
viewModel.navigationEvent.collect { event ->
event?.getContentIfNotHandled()?.let { route ->
navController.navigate(route)
}
}
}

优点:

确保事件只被消费一次,适合复杂场景。

最佳实践总结

做法 是否推荐 说明
ViewModel 通过 SharedFlow 发出导航事件 ✅ 强烈推荐 解耦、安全、符合单向数据流
使用 Event<T> 包装一次性事件 ✅ 推荐 防止重复消费