Android Kotlin协程:等待协程完成后获取ViewModel变量问题
解决Kotlin协程异步导致ViewModel状态未及时更新的问题
你遇到的问题非常典型——协程的异步执行特性导致主线程提前读取了未更新的validEmail变量。sendAuthCode()里的数据库查询和邮件发送逻辑是在viewModelScope.launch开启的后台协程中运行的,而主线程调用完sendAuthCode()后会立刻执行后续的if(!validEmail)判断,这时候后台协程还没跑完,validEmail自然还是初始的false。
直接用join()等待协程完成是不可取的,因为如果在主线程调用join()会阻塞UI线程,导致界面卡顿甚至ANR(应用无响应)。下面给你两种符合Android架构规范的解决方案:
方案1:用LiveData/StateFlow观察状态变化(推荐)
ViewModel的核心职责之一是管理UI状态,直接暴露可变变量让Fragment同步读取不符合架构设计。更好的方式是用LiveData(或更现代的StateFlow)封装状态,让Fragment观察状态变化:
修改后的ViewModel代码
class ForgetPasswordViewModel(val repository: Repository) : ViewModel() { lateinit var emailAddress: String // 用MutableLiveData封装内部状态,对外暴露不可变的LiveData private val _validEmail = MutableLiveData<Boolean>() val validEmail: LiveData<Boolean> = _validEmail fun sendAuthCode() { viewModelScope.launch { val userData = repository.getUserDataByEmail(emailAddress) if (userData != null) { val authCode = userData.authCode val sender = SendMail( AppContext.appContext!!, emailAddress, "Password reset code", "Dear ${userData.firstName},\n\n" + " This is your reset code.\n\n" + "Password reset code: $authCode \n" ) sender.execute() // 更新LiveData状态,自动通知观察者 _validEmail.value = true } else { // 处理邮箱不存在的情况,更新状态 _validEmail.value = false } } } }
修改后的Fragment代码
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // 观察validEmail的状态变化,在状态更新时处理UI逻辑 forgetPasswordViewModel.validEmail.observe(viewLifecycleOwner) { isValid -> if (isValid) { Toast.makeText(context, "Password reset code sent successfully!", Toast.LENGTH_SHORT).show() } else { Toast.makeText(context, "Email not found in the system", Toast.LENGTH_SHORT).show() } } viewBinding.btnRequestPassword.setOnClickListener { val email = emailAddress.text.toString() forgetPasswordViewModel.emailAddress = email if (email.length < 3) { ValidationChecks.showToast("Enter valid email address", context) } else { // 只触发发送逻辑,UI反馈交给观察者处理 forgetPasswordViewModel.sendAuthCode() } } }
方案2:将sendAuthCode改为挂起函数,在Fragment协程中调用
如果你更倾向于同步式的代码写法,可以把sendAuthCode改成挂起函数,然后在Fragment的lifecycleScope协程中调用,等待协程完成后再处理UI:
修改后的ViewModel代码
class ForgetPasswordViewModel(val repository: Repository) : ViewModel() { lateinit var emailAddress: String // 改成挂起函数,直接返回操作结果 suspend fun sendAuthCode(): Boolean { val userData = repository.getUserDataByEmail(emailAddress) return if (userData != null) { val authCode = userData.authCode val sender = SendMail( AppContext.appContext!!, emailAddress, "Password reset code", "Dear ${userData.firstName},\n\n" + " This is your reset code.\n\n" + "Password reset code: $authCode \n" ) sender.execute() true } else { false } } }
修改后的Fragment代码
viewBinding.btnRequestPassword.setOnClickListener { val email = emailAddress.text.toString() forgetPasswordViewModel.emailAddress = email if (email.length < 3) { ValidationChecks.showToast("Enter valid email address", context) } else { // 在Fragment的生命周期协程中调用挂起函数,等待结果返回 lifecycleScope.launch { val isSuccess = forgetPasswordViewModel.sendAuthCode() if (isSuccess) { Toast.makeText(context, "Password reset code sent successfully!", Toast.LENGTH_SHORT).show() } else { Toast.makeText(context, "Email not found in the system", Toast.LENGTH_SHORT).show() } } } }
额外优化点
你ViewModel里存在一个冗余问题:已经通过构造函数注入了repository,但又在内部创建了val repo = Repository(),建议统一使用构造函数传入的实例,避免重复创建Repository对象。
内容的提问来源于stack exchange,提问作者Salik Tariq
相关产品推荐
相关产品推荐

