Jetpack Compose调用viewModelScope方法后如何等待协程完成再导航
问题根源
现有写法的核心问题是createUser内通过viewModelScope.launch启动的协程是异步非阻塞的,Compose的onClick回调在调用vm.createUser(context)后不会等待协程执行完毕,会直接执行后续的导航逻辑,此时接口请求尚未返回,自然拿不到正确的响应结果。
方案1:基于UI状态驱动导航(推荐,符合MVVM分层规范)
这种方案把请求逻辑完全收敛在ViewModel层,UI层只负责观察状态做对应UI操作,职责边界更清晰。
- 首先定义注册流程的UI状态密封类:
sealed class RegisterUiState { object Idle : RegisterUiState() // 初始空闲状态 object Loading : RegisterUiState() // 请求加载中 data class Success(val data: RegisterResponse) : RegisterUiState() // 请求成功 data class Error(val msg: String) : RegisterUiState() // 请求失败 }
- 修改ViewModel代码,替换原有的response变量为状态流,在请求各阶段更新状态:
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow private val _registerUiState = MutableStateFlow<RegisterUiState>(RegisterUiState.Idle) val registerUiState = _registerUiState.asStateFlow() // 对外暴露不可变的状态流 fun createUser(context: Context) { // 发起请求前先更新为加载状态,UI可据此显示加载态、拦截重复点击 _registerUiState.value = RegisterUiState.Loading viewModelScope.launch { runCatching { val jsonObject = JSONObject().apply { put("correo", register.value.correo) put("password", register.value.password) put("nombre", register.value.nombre) put("apellido", register.value.apellido) put("direccion", register.value.direccion) put("telefono", register.value.telefono) } val bodyRequest = jsonObject.toString().toRequestBody() registerService.register(bodyRequest) }.onSuccess { result -> _registerUiState.value = RegisterUiState.Success(result) }.onFailure { e -> _registerUiState.value = RegisterUiState.Error(e.message ?: "注册请求失败,请稍后重试") } } } // 重置状态的方法,处理完事件后调用避免重复触发 fun resetRegisterState() { _registerUiState.value = RegisterUiState.Idle }
- 在Compose侧收集状态,通过副作用处理导航、Toast等一次性事件,避免重组时重复触发:
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.LaunchedEffect // 收集UI状态 val uiState by vm.registerUiState.collectAsStateWithLifecycle() LaunchedEffect(uiState) { when(val state = uiState) { is RegisterUiState.Success -> { // 拿到成功结果做导航 Log.d("result from compose", "${state.data.message}") navController.navigate(Screen.login.route) { popUpTo(Screen.login.route) { inclusive = true } } // 处理完成后重置状态为空闲,避免后续重组重复触发导航 vm.resetRegisterState() } is RegisterUiState.Error -> { // 弹出错误提示 Toast.makeText(context, state.msg, Toast.LENGTH_SHORT).show() vm.resetRegisterState() } RegisterUiState.Loading -> { // 可在此处显示全局加载弹窗,拦截用户操作 } RegisterUiState.Idle -> {} } } // 按钮onClick逻辑简化 onClick = { // 加载中直接拦截点击,避免重复请求 if (vm.registerUiState.value is RegisterUiState.Loading) return@onClick if (vm.validateCredentials() == "ok") { vm.createUser(context) } else { showPopUp = !showPopUp } }
方案2:将createUser改为挂起函数,在UI侧协程中等待结果
如果不想引入额外的状态类,也可以直接把请求逻辑改为挂起函数,在UI层的协程作用域中调用等待结果,写法更直接。
- 修改ViewModel的createUser方法为挂起函数:
suspend fun createUser(context: Context): Result<RegisterResponse> { return runCatching { val jsonObject = JSONObject().apply { put("correo", register.value.correo) put("password", register.value.password) put("nombre", register.value.nombre) put("apellido", register.value.apellido) put("direccion", register.value.direccion) put("telefono", register.value.telefono) } val bodyRequest = jsonObject.toString().toRequestBody() registerService.register(bodyRequest) } }
- 修改Compose侧代码,通过
rememberCoroutineScope启动协程调用挂起函数:
import androidx.compose.runtime.rememberCoroutineScope val scope = rememberCoroutineScope() onClick = { if (vm.validateCredentials() == "ok") { scope.launch { val result = vm.createUser(context) result.onSuccess { resp -> Log.d("result from compose", "${resp.message}") navController.navigate(Screen.login.route) { popUpTo(Screen.login.route) { inclusive = true } } }.onFailure { e -> Toast.makeText(context, e.message ?: "注册失败,请稍后重试", Toast.LENGTH_SHORT).show() } } } else { showPopUp = !showPopUp } }
注意事项:
- 所有网络请求必须加异常捕获,避免未处理异常导致应用崩溃
- 请求过程中必须做防重复点击处理,避免用户短时间多次点击触发重复请求
- 导航、Toast这类一次性事件不要直接在重组流程中调用,要通过副作用(如LaunchedEffect)触发,避免重复执行
内容的提问来源于stack exchange,提问作者sofnomic cr
相关产品推荐
相关产品推荐

