1

I am reading about Kotlin coroutine in Google 's documentation. I'm adviced to use withContext(Dispacher.IO) to a different thread to main-safety. But I have a problem , fetchData() done before response from server so fetchData() return null result. Any help that I appreciate.

https://developer.android.com/kotlin/coroutines/coroutines-best-practices#main-safe

class GameRemoteDataSource @Inject constructor(val api : GameApi) {
    val IODispatcher: CoroutineDispatcher = Dispatchers.IO

    suspend fun fetchData() : Resource<ListGameResponse> {
        var resource : Resource<ListGameResponse> = Resource.loading(null)
        withContext(IODispatcher){
            Log.d("AAA Thread 1", "${Thread.currentThread().name}")
            api.getAllGame(page = 1).enqueue(object : Callback<ListGameResponse>{
                override fun onResponse(
                    call: Call<ListGameResponse>,
                    response: Response<ListGameResponse>
                ) {
                    if(response.code()==200){
                        resource = Resource.success(response.body())
                    }else{
                        resource = Resource.success(response.body())
                    }
                    Log.d("AAA code",response.code().toString())
                }
                override fun onFailure(call: Call<ListGameResponse>, t: Throwable) {
                    resource = Resource.error(t.message.toString(),null)
                    Log.d("AAA Thread", "${Thread.currentThread()}")
                }

            })
            Log.d("AAA Thread", "${Thread.currentThread()}")
            Log.d("AAA resource",resource.data.toString()+ resource.status.toString())
        }
        return resource
    }
}
  • Does this answer your question? [Existing 3-function callback to Kotlin Coroutines](https://stackoverflow.com/questions/48552925/existing-3-function-callback-to-kotlin-coroutines) – broot Aug 05 '21 at 11:18
  • While that question's answers could potentially be used to solve this problem, it doesn't explain why the OP's code doesn't work, and it's unnecessary to use `suspendCoroutine` when using Retrofit, which already has suspend functions built in. – Tenfour04 Aug 05 '21 at 12:47

2 Answers2

1

withContext is not helpful for converting an asynchronous function with callback into suspending code that can be used in a coroutine. It is more applicable to converting synchronous blocking code. Your non-working strategy of creating an empty variable and trying to fill it in the callback to synchronously return is described in the answers to this question.

For an asynchronous function with callback, if it returns a single value like your code above, this is typically converted to a suspend function using suspendCoroutine or suspendCancellableCoroutine. If it returns a series of values over time (calls the callback multiple times), it would be fitting to use callbackFlow to convert it to a Flow that can be collected in a coroutine.

But it looks like you're using Retrofit, which already has a suspend function alternatives to enqueue so you don't need to worry about all this. You can use the await() or awaitResponse() functions instead. In this case, await() would return ListGameResponse and awaitResponse() would return Response<ListGameResponse>. So awaitResponse() is better if you need to check the response code.

Awaiting returns the response and throws an exception if there's an error, so you can use try/catch instead of adding a failure listener.

class GameRemoteDataSource @Inject constructor(val api : GameApi) {

    suspend fun fetchData(): Resource<ListGameResponse> {
        return try {
            val response = api.getAllGame(page = 1).awaitResponse()
            Log.d("AAA code", response.code().toString())
            Resource.success(response.body())
        } catch (exception: Exception) {
            Resource.error(exception.message.toString(),null)
        }
    }

}
Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Hi, first I appreciate your help . In my case I want follow the advice from Google' s documentation and I want to solve this problem to understand clearly. I understood why it return null. As you metioned above, "withContext is not helpful for converting an asynchronous function " so I decide use execute() instead of enqueue() because execute() is synchronized function. – Trần Văn Hiền Aug 05 '21 at 14:31
  • While `execute` is a theoretically acceptable, since Retrofit already supplies an `await()` suspend function, there is no reason to use `execute()`. Google's suggestion in the documentation is only applicable when you are using an API that does not provide suspend functions. Using `withContext(Dispatchers.IO) { execute() }`, works, but it **most definitely is not** best practice in this situation. It ties up an extra thread for no reason. – Tenfour04 Aug 05 '21 at 15:27
  • Note the text in the Google Link "**if** a class is doing long-running **blocking** operations in a coroutine". If you use the `await()` or `awaitResponse()` suspend function, it is not blocking, so the advice does not apply. The point of that paragraph is that your suspend functions should not block, which is a general convention in Kotlin anyway. – Tenfour04 Aug 05 '21 at 15:35
  • As you mentioned "If you use the await() or awaitResponse() suspend function, it is not blocking, so the advice does not apply". I wonder "blocking" that block current thread to start new thread ? ( In this case new thread is call api ) – Trần Văn Hiền Aug 06 '21 at 04:42
  • 1
    Blocking means calling the function blocks the current thread, like `execute` does. `enqueue` is non-blocking because it does not block the current thread, although it does internally use another thread, depending on VM version. And `await` is non-blocking because it suspends. – Tenfour04 Aug 06 '21 at 04:53
  • Thank you, it's really helpful for me . I just want to understand it clearly but I am newbie and it' s so difficult ..... – Trần Văn Hiền Aug 06 '21 at 05:20
0

You should use suspendCancellableCoroutine to convert asynchronous API into a coroutine flow, like this

suspend fun fetchData(): ListGameResponse = withTimeout(Duration.seconds(60)) {
  suspendCancellableCoroutine<ListGameResponse> { cont ->
    api.getAllGame(page = 1).enqueue(object : Callback<ListGameResponse> {
      override fun onResponse(
          call: Call<ListGameResponse>,
          response: Response<ListGameResponse>
      ) {
        Log.d("AAA code", response.code().toString())
        cont.resume(response.body())
      }

      override fun onFailure(call: Call<ListGameResponse>, t: Throwable) {
        cont.resumeWithException(t)
      }
    })
  }
}
Roman Truba
  • 4,401
  • 3
  • 35
  • 60