Jetpack Compose中带ViewModel协程的页面导航正确实现方式
问题解决:ViewModel协程因页面导航销毁未执行的问题
核心原因
点击完成按钮后,先执行navHostController.popBackStack()和navigate操作,直接将WelcomeScreen从导航栈移除,导致关联的WelcomeScreenViewModel被销毁。由于viewModelScope是生命周期感知组件,在ViewModel的onClear()方法调用时会取消所有关联协程,保存状态的协程还未启动就被终止。
可行解决方案
方案1:用NonCancellable确保保存操作完成
在ViewModel的协程中,将保存逻辑包裹在withContext(NonCancellable)中,即使ViewModel被销毁,这部分代码也能执行完毕。
修改后的WelcomeScreenViewModel代码:
@HiltViewModel class WelcomeScreenViewModel @Inject constructor(private val useCase: UseCase): ViewModel(){ private val TAG = "WelcomeScreenViewModel" fun welcomePageCompleted(isCompleted: Boolean){ viewModelScope.launch(Dispatchers.IO) { try { withContext(NonCancellable) { useCase.welcomePageCompletedUseCase(isCompleted = isCompleted) Log.i(TAG, "Coroutine triggered! ") Log.i(TAG,"welcome page state saved! $isCompleted") } } finally { // 可选:添加清理逻辑 } } } }
同时调整按钮点击逻辑的顺序,先调用ViewModel的保存方法,再执行导航:
FinishButton( modifier = Modifier.weight(1f), pagerState = pagerState, lastPage = pages.size - 1 ) { welcomeViewModel.welcomePageCompleted(isCompleted = true) navHostController.popBackStack() navHostController.navigate(Screen.Home.route) }
方案2:通过Flow通知UI导航(更推荐)
让ViewModel在保存完成后通知UI执行导航,确保保存操作完全完成后再销毁页面。
修改ViewModel,添加Flow用于导航通知:
@HiltViewModel class WelcomeScreenViewModel @Inject constructor(private val useCase: UseCase): ViewModel(){ private val TAG = "WelcomeScreenViewModel" private val _navigateToHome = MutableSharedFlow<Unit>() val navigateToHome = _navigateToHome.asSharedFlow() fun welcomePageCompleted(isCompleted: Boolean){ viewModelScope.launch(Dispatchers.IO) { useCase.welcomePageCompletedUseCase(isCompleted = isCompleted) Log.i(TAG, "Coroutine triggered! ") Log.i(TAG,"welcome page state saved! $isCompleted") // 保存完成后发送导航信号 _navigateToHome.emit(Unit) } } }
在WelcomeScreen中收集Flow并执行导航:
@Composable fun WelcomeScreen( navHostController: NavHostController, welcomeViewModel: WelcomeScreenViewModel = hiltViewModel() ) { // 原有代码保持不变... // 监听导航通知 LaunchedEffect(Unit) { welcomeViewModel.navigateToHome.collect { navHostController.popBackStack() navHostController.navigate(Screen.Home.route) } } FinishButton( modifier = Modifier.weight(1f), pagerState = pagerState, lastPage = pages.size - 1 ) { // 仅触发保存操作,导航由Flow通知执行 welcomeViewModel.welcomePageCompleted(isCompleted = true) } }
之前尝试无效的原因
- 用
rememberCoroutineScope()包裹导航:该Scope属于Composable的生命周期,页面被移除时Scope会被取消,导致导航操作失效。 - 先调用ViewModel方法再导航:虽然顺序正确,但ViewModel协程启动需要时间,导航操作立即执行导致ViewModel被销毁,协程仍会被取消;而
NonCancellable或Flow通知的方式能规避这个问题。
内容的提问来源于stack exchange,提问作者Abdelaziz Faki
相关产品推荐
相关产品推荐

