如何将暂停函数的 return 值存储到变量中?

How to store the return value of suspended function to a variable?

我正在尝试了解 Kotlin 协程。所以这是我的代码(基于 this tutorial)。为了保持代码相对简单,我特意避免了 MVVM、LiveData 等。只是 Kotlin couroutine 和 Retrofit。

考虑这个登录过程。

ApiInterface.kt

interface ApiInterface {

    // Login
    @POST("/user/validate")
    suspend fun login(@Body requestBody: RequestBody): Response<ResponseBody>

}

ApiUtil.kt

class ApiUtil {

    companion object {

        var API_BASE_URL = "https://localhost:8100/testApi"

        fun getInterceptor() : OkHttpClient {
            val logging = HttpLoggingInterceptor()

            logging.level = HttpLoggingInterceptor.Level.BODY

            val okHttpClient = OkHttpClient.Builder()
                .addInterceptor(logging)
                .build()

            return  okHttpClient
        }

        fun createService() : ApiInterface {

            val retrofit = Retrofit.Builder()
                .client(getInterceptor())           
                .addConverterFactory(GsonConverterFactory.create())
                .baseUrl(OJIRE_BASE_URL)
                .build()
            return retrofit.create(ApiInterface::class.java)

        }
    }

   
    fun login(userParam: UserParam): String {
        val gson = Gson()
        val json = gson.toJson(userParam)
        var resp = ""
        val requestBody = json.toString().toRequestBody("application/json".toMediaTypeOrNull())

        CoroutineScope(Dispatchers.IO).launch {
            val response = createService().login(requestBody)

            withContext(Dispatchers.Main){
                if (response.isSuccessful){
                    val gson = GsonBuilder().setPrettyPrinting().create()
                    val prettyJson = gson.toJson(
                        JsonParser.parseString(
                            response.body()
                                ?.string()
                        )
                    )
                    resp = prettyJson
                    Log.d("Pretty Printed JSON :", prettyJson)
                }
                else {
                    Log.e("RETROFIT_ERROR", response.code().toString())
                }
            }
        }

        return resp
    }
}

LoginActivity.kt

class LoginActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
    
        edtUsername = findViewById(R.id.edtUsername)
        edtPassword = findViewById(R.id.edtPassword)
        btnLogin = findViewById(R.id.btnLogin)
        
        btnLogin.setOnClickListener {
            val api = ApiUtil()
            val userParam = UserParam(edtMobileNo.text.toString(), edtPassword.text.toString())
            val response = JSONObject(api.login(userParam))
            var msg = ""
            
            if (response.getString("message").equals("OK")){
                msg = "Login OK"
            }
            else {
                msg = "Login failed"
            }
            
            Toast.makeText(applicationContext, msg, Toast.LENGTH_SHORT).show()
    
        }
    }
}

调试登录 activity 时,API 响应在 prettyJson 上被正确捕获 问题是 resp 仍然是空的。猜猜这就是异步进程的工作方式。我想要的是等到 API 调用完成,然后结果可以很好地传递给 resp 作为 login() 的 return 值。怎么做?

好吧,你在这里有几处错误。我们会尽力解决所有问题。

首先,你描述的主要问题是需要在login()中同步获取resp。你遇到这个问题只是因为你首先在那里启动了一个异步操作。解决方案?不要那样做,通过删除 launch() 同步获取响应。我想 withContext() 也不是必需的,因为我们不做任何需要主线程的事情。删除它们后,代码变得更加简单且完全同步。

我们需要对 login() 做的最后一件事是让它可以暂停。它需要等待请求完成,所以它是一个挂起函数。结果 login() 应该类似于:

suspend fun login(userParam: UserParam): String {
    val gson = Gson()
    val json = gson.toJson(userParam)
    val requestBody = json.toString().toRequestBody("application/json".toMediaTypeOrNull())

    val response = createService().login(requestBody)

    return if (response.isSuccessful){
        val gson = GsonBuilder().setPrettyPrinting().create()
        gson.toJson(
            JsonParser.parseString(
                response.body()
                    ?.string()
            )
        )
    }
    else {
        Log.e("RETROFIT_ERROR", response.code().toString())
        // We need to do something here
    }
}

现在,当我们将 login() 转换为可暂停时,我们无法直接从侦听器调用它。这里我们确实需要启动异步操作,但我们不会像您在示例中那样使用 CoroutineScope(),因为它会泄漏后台任务和内存。我们将像这样使用 lifecycleScope

btnLogin.setOnClickListener {
    val api = ApiUtil()
    val userParam = UserParam(edtMobileNo.text.toString(), edtPassword.text.toString())
    
    lifecycleScope.launch {
        val response = JSONObject(api.login(userParam))
        var msg = ""

        if (response.getString("message").equals("OK")){
            msg = "Login OK"
        }
        else {
            msg = "Login failed"
        }

        withContext(Dispatchers.Main) {
            Toast.makeText(applicationContext, msg, Toast.LENGTH_SHORT).show()
        }
    }
}

以上代码可能无法完全发挥作用。如果没有所有必需的数据结构等,很难提供工作示例。但我希望你明白这一点。

此外,您的代码中还有其他一些可以改进的地方,但我没有触及它们以免让您感到困惑。