Kotlin协程如何获取挂起函数返回值?登录逻辑报Already resumed崩溃
崩溃根因拆解
Already resumed 是Kotlin协程使用suspendCoroutine时最常见的硬错误,本质是同一个挂起的continuation实例被多次调用了resume方法,协程规范要求continuation必须有且仅有一次resume调用,多一次就会直接抛异常。
对应代码,触发崩溃的完整时序是:
- 协程逻辑进入
suspendCoroutine的lambda块,首先校验凭证有效性 - 当凭证有效时,代码执行
viewModelScope.launch{...}——这个方法是完全异步不阻塞的,调用后会立刻返回,根本不会等待内部网络请求执行完成 - lambda块执行完launch语句后,直接走到块的最后一行,无条件执行
it.resume(false),这是第一次resume调用 - 延迟一段时间后,网络请求完成,之前启动的异步协程拿到登录成功的结果,又执行了一次
it.resume(true),这是第二次resume,直接触发崩溃
除此之外代码还存在几个逻辑硬伤:
- 完全没必要使用
suspendCoroutine做封装:remoteDataSource.login本身就是挂起函数,不需要把它再套一层回调式的转换,平白增加出错概率 - 异常捕获逻辑完全失效:try-catch写在
launch语句的外层,但是launch启动的协程内部的异常根本不会向外层冒泡,抓不到任何网络请求的错误 - 分支逻辑没有做互斥拦截:校验不通过、登录成功、登录失败的多个分支都可能触发resume,没有做条件判断保证分支唯一
- Fragment中的调用没有接收login的返回值,就算登录成功也拿不到结果做后续导航操作
重构后的正确实现
因为所有依赖接口都是挂起函数,完全不需要套suspendCoroutine和异步launch,直接顺序写逻辑就行——挂起函数本身就可以在协程内顺序等待网络请求结果:
class LoginViewModel @ViewModelInject constructor( private val remoteDataSource: OrderRemoteDataSource, private val sessionManager: SessionManager // 补充代码中遗漏的依赖声明 ) : ViewModel() { private fun areValidCredentials(username: String?, password: String?): Boolean { return !username.isNullOrEmpty() && !password.isNullOrEmpty() && username.length > 4 && password.length > 4 } /** * 登录挂起函数:返回值表示登录是否成功 */ suspend fun login(username: String?, password: String?): Boolean { // 凭证校验不通过,直接返回 if (!areValidCredentials(username, password)) { Toast.makeText( LOGIN_ACTIVITY, "Username and password must be at least 5 letters long", Toast.LENGTH_SHORT ).show() return false } // 直接顺序执行网络请求,不需要额外启动协程 return try { val loginResponse = remoteDataSource.login(LoginRequest(username, password)) if (loginResponse.status == Resource.Status.SUCCESS) { val jwtToken = loginResponse.data?.jwtToken if (!jwtToken.isNullOrEmpty()) { sessionManager.saveAuthToken(jwtToken) return true } } // 所有非成功场景统一返回false false } catch (e: Exception) { Log.e("LoginViewModel", "Login error: ${e.message}", e) false } } }
调用端适配
原来的Fragment调用时需要接收login函数的返回值,再执行后续的导航逻辑,注意Fragment中要用viewLifecycleOwner.lifecycleScope避免视图销毁后的内存泄漏:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) navController = findNavController() loginButton.setOnClickListener { viewLifecycleOwner.lifecycleScope.launch { val loginResult = mViewModel.login( loginUsernameText.text.toString(), loginPasswordText.text.toString() ) // 根据登录结果做后续操作 if (loginResult) { navController.navigate(R.id.action_login_to_home) } } } }
协程使用注意要点
- 只有把传统回调式的Java API转换成挂起函数时,才需要使用
suspendCoroutine/suspendCancellableCoroutine,并且必须保证所有分支只会触发一次resume,绝对不能在resume之后还有其他路径会再次调用resume方法 - 挂起函数本身不阻塞线程,也不需要在内部再调用
launch新启协程——调用这个函数的上下文本身就运行在协程中,顺序写代码即可等待挂起结果 - 避免在挂起函数内部启动和当前调用无关的独立协程,不然会破坏挂起函数的时序一致性,导致返回值无法正确传递给调用方
内容的提问来源于stack exchange,提问作者WBLord
相关产品推荐
相关产品推荐

