Home > Software design >  How to store the return value of suspended function to a variable?
How to store the return value of suspended function to a variable?

Time:10-09

I'm trying to understand Kotlin couroutine. So here's my code (based on this tutorial). To keep the code relatively simple, I deliberately avoid MVVM, LiveData, etc. Just Kotlin couroutine and Retrofit.

Consider this login process.

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()
    
        }
    }
}

When debugging the login activity, the API response is captured properly on prettyJson The problem is resp is still empty. Guess that's how async process work. What I want is to wait until the API call is completed, then the result can be nicely passed to resp as the return value of login(). How to do that?

CodePudding user response:

Well, you got several things wrong here. We'll try to fix them all.

First, the main problem you described is that you need to acquire resp in login() synchronously. You got this problem only because you first launched an asynchronous operation there. Solution? Don't do that, get the response synchronously by removing launch(). I guess withContext() is also not required as we don't do anything that requires the main thread. After removing them the code becomes much simpler and fully synchronous.

Last thing that we need to do with login() is to make it suspendable. It needs to wait for the request to finish, so it is a suspend function. The resulting login() should be similar to:

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
    }
}

Now, as we converted login() to suspendable, we can't invoke it from the listener directly. Here we really need to launch asynchronous operation, but we won't use CoroutineScope() as you did in your example, because it leaked background tasks and memory. We will use lifecycleScope like this:

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()
        }
    }
}

Above code may not be fully functional. It is hard to provide working examples without all required data structures, etc. But I hope you get the point.

Also, there are several other things in your code that could be improved, but I didn't touch them to not confuse you.

  • Related