Search code examples
androidkotlinretrofitretrofit2kotlin-coroutines

How does the following code work on Android


In one of Philip Lackner's tutorial, he is making the network call and UI changes like following:

        lifecycleScope.launchWhenCreated {
        binding.progressBar.isVisible = true
        val response = try {
            RetrofitInstance.api.getTodos()
        } catch(e: IOException) {
            Log.e(TAG, "IOException, you might not have internet connection")
            binding.progressBar.isVisible = false
            return@launchWhenCreated
        } catch (e: HttpException) {
            Log.e(TAG, "HttpException, unexpected response")
            binding.progressBar.isVisible = false
            return@launchWhenCreated
        }
        if(response.isSuccessful && response.body() != null) {
            todoAdapter.todos = response.body()!!
        } else {
            Log.e(TAG, "Response not successful")
        }
        binding.progressBar.isVisible = false
    }

Here is the pointer to his code: https://github.com/philipplackner/RetrofitCrashCourse/blob/master/app/src/main/java/com/plcoding/retrofitcrashcourse/MainActivity.kt

lifecycleScope is by default tied to main thread which means unless we change the scope, the retrofit network call in the above code is going to be run on main thread.

Is this statement correct, if not why?


Solution

  • It's a bit complicated, but I'll try to explain it based on what I've found out during my spelunking in the Retrofit code.

    Yes, you are indeed running the call RetrofitInstance.api.getTodos() on the main dispatcher (implicitly via lifecycleScope), but...

    No, you aren't running it on the main thread. Under the hood, suspend functions in Retrofit are implemented by using a combination of a suspendCancellableCoroutine and enqueue(...) as seen in the source code itself:

    suspend fun <T> Call<T>.awaitResponse(): Response<T> {
      return suspendCancellableCoroutine { continuation ->
        continuation.invokeOnCancellation {
          cancel()
        }
        enqueue(object : Callback<T> {
          override fun onResponse(call: Call<T>, response: Response<T>) {
            continuation.resume(response)
          }
    
          override fun onFailure(call: Call<T>, t: Throwable) {
            continuation.resumeWithException(t)
          }
        })
      }
    }
    

    With that in mind, enqueue(...) will eventually (somewhere down the rabbit hole) do the actual network request on a another executor, and thus it's doesn't cause any NetworkOnMainThread violations when you call it from the main dispatcher. Why? Well, it doesn't actually do any network calls on the main thread, per se.

    Hope that clarifies it a bit. :)