Improving App Network Architecture With Retrofit And Kotlin Coroutine Call Adapter

Almost in every Android mobile application, we have to deal with network calls and most of the time we end up using Retrofit. No doubt Retrofit is our favorite library when it comes to networking. Kotlin and now coroutine has made Networking on Android even easier than before.

I’ve recently listened to this talk by Florina Muntenescu. In this video, she talked about how Kotlin Couroutin Call Adapter made networking easy when working with Retrofit. In order to work with Kotlin Coroutine Call Adapter, there is a Call Adapter created by Jake Wharton which uses Kotlin Coroutine Deferred type.

What You’ll Learn In This Article

  1. We’ll see how to use sealed class for Result both in a successful request and in error case.
  2. Add the Kotlin Coroutine Call Adapter when creating a Retrofit instance.
  3. We’ll see how to create a safe top-level function for every request which we made through Retrofit.
  4. Json arrives with its values and we’ll see how to parse the response.

In order to begin let’s add the following dependencies into our app level build.gradle file:

// ***** Networking Dependencies ***** //
    implementation 'com.squareup.retrofit2:retrofit:2.3.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1'
    implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-experimental-adapter:1.0.0'

App Intro

I’ve built a Movie Searcher application. In this app, we gonna simply hit an url and download some Json content and show them in a GridView. In order to get the response, we are going to use Retrofit and Kotlin Coroutine Call Adapter.

Kotlin Coroutine Call Adapter Retrofit Gif

Declaring Our Interface Or ApiService

For the above example, I used an API from TheMovieDb for demonstration. The following shows the ApiService interface.

interface ApiService {

    @GET(value = "popular")
    fun getPopular(
        @Query(
            value = "api_key"
        ) apiKey: String, @Query(
            value = "page"
        ) page: Int
    ): Deferred<Response<MovieResponse>>
}

You may have noticed that instead of Call<T>, we now have Deferred<T> defined in our interface function. Now our functions return a Deferred value rather than a Response.

The Deferred usually used with await suspending function in order to wait for the result without blocking the main/current thread.

Creating The Retrofit Service

In order to use Kotlin Coroutine Call Adapter, we need to add the instance of its factory when building our Retrofit ApiService.


companion object {
    private const val BASE_URL = "https://api.themoviedb.org/3/movie/"
}

fun getServiceApi(retrofit : Retrofit) = retrofit.create(ApiService::class.java)

fun getRetrofit() = Retrofit.Builder()
            .addCallAdapterFactory(CoroutineCallAdapterFactory())
            .addConverterFactory(GsonConverterFactory.create())
            .baseUrl(BASE_URL)
            .client(okHttpClient)
            .build()

val apiService = getServiceApi(getRetrofit())

Now we have Retrofit service instance with our Kotlin Coroutine Call Adapter Factory.

Sealed Classes For Result

If you guys have remembered, at the start of this article I said we’re gonna see how to use sealed classes to wrap-up the response in Sucess and in the Error case. You see the fact that we got the data either with Sucess or with Error and the way to do this is with sealed classes.

sealed class Result<out T: Any> {
    data class Success<out T : Any>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}

Executing The Network Request

So for the implementation, we have MovieDataSource that would depend on the ApiService.

class MovieDataSource constructor(private val apiService: ApiService) {

     suspend fun getMovies(pageNumber : Int) : Result<MovieResponse>{
        val response = apiService.getPopular(API_KEY, pageNumber).await()
        if (response.isSuccessful)
            return Result.Success(response.body())
        return Result.Error(IOException("Error occurred during fetching movies!"))
     }

}

In order to fetch movies we would have a suspension function that would get us a parameter a pageNumber for the popular movies and with that, we return a Result of MovieResponse. In the getMovie method, we trigger a request to the back-end and wait for the response and in the end, we handle Success or Failure of the response.

If you wanna know how the kotlin suspended function works under the hood, do check out this article.

I know I know many of you saying what if an error occurred during network request. Yeah! we need to wrap this request inside a try/catch block.

class MovieDataSource constructor(private val apiService: ApiService) {

     suspend fun getMovies(pageNumber : Int) : Result<MovieResponse>{
        val response = apiService.getPopular(API_KEY, pageNumber).await()
        try {
             if (response.isSuccessful)
                   return Result.Success(response.body())
             return Result.Error(IOException("Error occurred during fetching movies!"))
        } catch(e : Exception){
            Result.Error(IOException("Unable to fetch movies!"))
        }
     }

}

By using the try/catch it looks like we handled the error case but for now every Network request we make we had to add this try/catch. Obviously, we did not want to do that.

Creating A SafeApiCall Top-level Function

For the sake of adding the try/catch to every Network request, we create a safeApiCall top-level function to just trigger the request.

suspend fun <T : Any> safeApiCall(call: suspend () -> Result<T>, errorMessage: String): Result<T> = try {
    call.invoke()
} catch (e: Exception) {
    Result.Error(IOException(errorMessage, e))
}

You see the safeApiCall is just a suspending function and most of all it also gets a suspending lambda as a parameter. So, inside this function, we just call the lambda, then in case, an error is thrown we just return the result of error based on the message that we pass as a parameter.

Update MovieDataSource Class

After creating a top-level suspending function we need to trigger our network request through the extension function.

class MovieDataSource constructor(private val apiService: ApiService) {

    suspend fun getMovies(pageNumber: Int) = safeApiCall(
        call = { popularMovies(pageNumber) },
        errorMessage = "Error occurred"
    )

    private suspend fun popularMovies(pageNumber: Int): Result<MovieResponse> {
        val response = apiService.getPopular(API_KEY, pageNumber).await()
        if (response.isSuccessful)
            return Result.Success(response.body()!!)
        return Result.Error(IOException("Error occurred during fetching movies!"))
    }
}

Now in our getMovies, we’ll just call safeApiCall and then we put our popularMovies method as a call and then we define an Error message.

By using the above approach we can focus on what matters when creating the network request and handling the response.

Interaction With The Suspending Function

So for the interaction with suspending function, we’re gonna work LiveData and ViewModel.

class MovieViewModel constructor(private val movieDataSource: MovieDataSource) : ViewModel() {

     // 1
     private val _movies = NonNullMediatorLiveData<List<MovieResponse.Movie>>()
     private val _error = NonNullMediatorLiveData<String>
     
     // 2
     val movies: LiveData<List<MovieResponse.Movie>> get() = _movies
     val erros : LiveData<String> get() = _error

     init {
        initGetMoviesCall()
     }
         
     // 3
     private fun initGetMoviesCall() {
        moviesJob = launch {
            val result = movieDataSource.getMovies(1)
            when (result) {
                is Result.Success -> _movies.postValue(value.data.movies)
                is Result.Error -> _error.postValue(value.exception.message)
            }
        }
     }      
}

Below is the explanation of the above code.

  1. I’m using NonNullMediatorLiveData class for _movies. The NonNullMediatorLiveData extends from MediatorLiveData class. The purpose of it extending from MediatorLiveData to make a lot easier to use, especially making it NonNull safe. The same thing goes for an error case.
  2. It is only for not just exposing our MediatorLiveData publically, we’ve given a public function to just get _movies and _errors. By doing this we’re keeping our immutability principle.
  3. In order to execute a suspending method, we need to trigger the request inside suspend block and launch is a suspending block. The launch launches the coroutine without blocking the current thread and returns a reference to the coroutine as a Job. Inside the launch block when the response came, we simply check if the result is Sucess then add data to movies and if it is an error then post the error message to _errors. Another thing to point out here is that you see we’re using the postValue method to set data, that’s because we’re inside the background thread context.

You can read more about how to work with LiveData and ViewModel together in this article.

MovieViewModelFactory

I believe you guys know that ViewModel by default doesn’t have any arguments that’s why we had to implement our own ViewModelFactory.

class MovieViewModelFactory constructor(private val movieDataSource: MovieDataSource) :
    ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>) = MovieViewModel(movieDataSource) as T
}

Cancel The Network Request

At some point, you may need to cancel the running async request because a user may rotate the device during the network request or maybe changing the current Activity. That’s why Job cancellation is always necessary!

override fun onCleared() {
    super.onCleared()
    moviesJob?.cancel()
}

// OR

fun cancelNetworkRequest() = moviesJob?.cancel()
 

NonNullMediatorLiveData

class NonNullMediatorLiveData<T> : MediatorLiveData<T>()

Interacting With UI

At this point, we have fully defined our backend API, ViewModel, and triggered a network request. Now we just need to listen to our movies or any error if they occurred.

private val movies = mutableListOf<MovieResponse.Movie>()

viewModel.movies
         .nonNull()
         .observe(this) {                
             this.movies.addAll(it)
             movieAdapter.notifyDataSetChanged()
         }

viewModel.error
         .nonNull()
         .observe(this) {
            showToast(it)
         }

You see in UI we’re simply observing our new movies. The UI does not need to know that, the movies are coming from the MockRepo or they came from the Network. It only knows how to show them.

Quick Note: The UI only needs to know how to show data not how to fetch it and perform business logic on it. All these things are kept under the hood.

Alright, guys, this was my demonstration on how we can improve our app network architecture when working with Kotlin Coroutine Call Adapter and Retrofit. If you guys want the source code of above app you can get it from GitHub.

If you’ve any advice on how to make our Networking Architecture better or how to extend the above example functionalities do comment below and discuss it. 🙃

LEAVE A REPLY

Please enter your comment!
Please enter your name here