【问题标题】:How to store the return value of suspended function to a variable?如何将挂起函数的返回值存储到变量中?
【发布时间】:2021-11-28 02:50:50
【问题描述】:

我正在尝试理解 Kotlin couroutine。所以这是我的代码(基于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()
    
        }
    }
}

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

【问题讨论】:

  • 您可以使用 resp:(String) -> Unit 作为 login() 函数中的额外参数,并在响应后在视图中捕获该参数(在 Activity 中)

标签: android kotlin kotlin-coroutines kotlin-android-extensions


【解决方案1】:

嗯,你在这里搞错了几件事。我们会尽力解决所有问题。

首先,您描述的主要问题是您需要同步获取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()
        }
    }
}

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

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

【讨论】:

  • 另外,我看到您使用的大多数误解实际上来自您链接的教程。我没有更好的,但我建议不要使用它。也许这只是我,但它充满了反模式。 CoroutineScope().launch() - 此代码泄漏后台任务和相关资源/内存。无需使用 Dispatchers.IO 来调用 Retrofit 挂起函数。也不需要仅仅因为我们完成 IO 而切换到Dispatchers.Main
  • 我明白了。 couroutines in not as simple as good old as good old'AsyncTask,肯定需要更多时间来研究这个概念。顺便说一句,如果 coroutine 没有包含在 lifecycleScope.launch 中,即使用户移动到不同的活动,它也会继续运行,是吗?
  • 其实我觉得它们更容易使用。它们只是需要获得的新知识,人们倾向于将它们与不适合他们的模式一起使用。但这绝对是有争议的——许多人认为它们很复杂。
  • 关于lifecycleScope.launch:是和否 ;-) 没有它,代码根本无法编译。我们不能从非挂起函数调用挂起函数。通常,我们使用runBlocking() 来连接这两个世界,它会阻塞调用线程,或者使用launch()/async() 异步启动操作。但是是的,你是对的,lifecycleScope 确保在我们离开活动时取消后台操作。 CoroutineScope().lauch() 不会这样做。
猜你喜欢
  • 2020-05-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-24
  • 2019-08-03
  • 1970-01-01
  • 2021-11-28
  • 1970-01-01
相关资源
最近更新 更多