You need to enable JavaScript to run this app. Kotlin协程如何获取挂起函数返回值?登录逻辑报Already resumed崩溃
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.11.15 10:36:04