Tag

ViewModel

Browsing

Coroutines are one of the ūüėć nicest language features in Kotlin. They provide a rather headache-free way to use the power of concurrency in your Kotlin program. I was very excited when I started playing around with them but eventually¬†came across a problem: what if you have nested coroutines (like a parent-child coroutine).

For instance, I came across this problem when I wanted to launch a coroutine and with-in of that coroutine I want to start multiple coroutines and when I cancel the parent coroutine the children needs to be canceled as well. How!

Gradle Stuff

Kotlin coroutine requires us to add some more stuff to our application modules build.gradle file. We start by adding the following dependencies right after the android section:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0-RC1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0-RC1'

In this article, I assume that you’ve some elementary proficiency with Kotlin coroutine. Now instead of directly jumping to above¬†problem, I wanna start this article by just launching a simple coroutine and cancel it when the ViewModel is going to destroy.

class RepositoryViewModel constructor(private val movieRepo : MovieRepo) : ViewModel() {
   
   private var movieRepoJob : Job? = null    
   
   fun getMovies() {
      movieRepoJob = launch {
           movieRepo.getPopularMovies()
           .... update view state
      }   
   }

   override fun onCleared(){
      movieRepoJob?.cancel()
   }
   
}

You see that’s its a very simple example, we have a ViewModel and in that there’s a getMovies method who simply¬†launch¬†a coroutine. Well, actually the¬†launch¬†returns a¬†Job¬†and that¬†Job¬†allows you to cancel it while it’s been running. So, you can simply cancel the¬†coroutine in the onCleared¬†method of ViewModel. Simple isn’t it! ūü§ó

Parent-Child Async Tasks

Now let’s get back to our previous example where we have more than one async tasks running. We have one parent¬†launch¬†block and inside that block, there’s another¬†async¬†task is running. So, how do we keep track of those tasks effectively when our ViewModel is going off the screen or we need to cancel them. Using this solution our previous example would look like:

class MyViewModel constructor(private val apiService : ApiService) : ViewModel() {
   
   private var parentJob : Job? = null    
   private var firstChildJob : Job? = null
   private var secondChildJob : Job? = null
   
   fun getMovies() {
      parentJob = launch {
           apiService.request1()
           firstChildJob = launch {
                apiService.request2()
                ........
           }
           secondChildJob = launch {
                apiService.request3()
                ........
           }
      }   
   }

   override fun onCleared(){
      parentJob?.cancel()
      firstChildJob?.cancel()
      secondChildJob?.cancel()
   }   
}

This above code works for our contrived example¬†but its hackiness makes me go ‚ÄúICK!‚ÄĚ. Currently, we’ve three¬†launch¬†and we have to keep track of three¬†Jobs¬†and, maybe later we need to have four¬†launch¬†and we need to keep track of four¬†Jobs. It gets little unwieldy and a bit too much code to actually maintain.¬†Trying to make this style work in any nontrivial program would be a complete nightmare.

To make the above example work in a conical way we need to have something like CompositeDisposable who can hold multiple other Disposable and dispose of them if any error occur or canceled them all with just one method. Happily! the kotlin coroutine library gives us the CoroutineScope.

Kotlin CoroutineScope

A CoroutineScope makes error handling easier, If any child coroutine fails, the entire scope fails and all of children coroutines are canceled. It should be implemented on entities with a well-defined lifecycle that is responsible for launching children coroutines. By entities, I mean Activity, Fragment, and ViewModel.

Every coroutine builder like launch and async are an extension of CoroutineScope and inherits corutineContext to automatically propagate both context elements and cancellation.

There are two ways to use to CoroutineScope in an application.

  • First, is to implement the¬†CoroutineScope on entities.
  • Second to use the coroutineScope top-level function which creates a new CoroutineScope and calls the specified¬†suspend¬†block with this scope.

1. Cancel Jobs By Implementing Kotlin CoroutineScope

Back to our previous parent-child coroutine example. Using CoroutineScope our example code looks like this:

class MyViewModel constructor(private val apiService : ApiService) : ViewModel(), CoroutineScope {   // 1
   // 2
   private val job = Job()
   // 3
   override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main
   // 4
   fun executeCalls() {
      launch(context = coroutineContext) {
           val firstRequestDeferred = async {
                apiService.request1()
           }
           val secondRequestDeffered = async {
                apiService.request2()
           }
           handleResponse(firstRequestDeferred,await(),secondRequestDeffered.await())
      }   
   }
   // 5
   override fun onCleared(){
       job.cancel()
   }   
}

Below is the explanation of the above code:

  1. As I said, we need to implement CoroutineScope so that our launch and  async invoked in the scope of an entity.
  2. We created this Job instance so that we can easily cancel all async call whenever our ViewModel is going to destroy.
  3.  We need to tell in which dispatchers our response is needed to dispatch.
  4. Note that an implementation of launch also defines an appropriate coroutineContext. Now the parent coroutine waits for all the coroutines inside their block to complete before completing themselves. If the ViewModel is going to destroy or any of the launched coroutines throws an exception then all the nested coroutines are canceled.
  5. Cancel Job in ViewModel onCleared method. After destroying, all children jobs will be canceled automatically.

Note:¬†You see with the above approach we don’t need to keep track so many Jobs¬†if we cancel the parent coroutine or any Exception¬†thrown inside any of the coroutines then all jobs will be canceled.

2. Cancel Jobs With CoroutineScope Top-level Function

// 1
suspend fun executeCall(apiService : ApiService) = couroutineScope {
   val firstDeferredRequest = async {
         apiService.request1()
   }
   val secondDeferredRequest = async {
        apiService.request2()
   }
   handleResponse(firstRequestDeferred.await(),secondDeferredRequest.await())
}

// 2 
val job = launch {
    executeCall()
}

// 3
job.cancel()

With structured concurrency, you have to wrap your code into a coroutineScope block that establishes a boundary of your operation, its scope. All the async coroutines become the children of this scope and, if the scope fails with an exception or is canceled, all the children are canceled, too.

If you wanna know how the kotlin suspended function works under the hood, do check out this article => Explore How Kotlin Coroutine Works Under The Hood.

That is all for now!

If you like this article, I appreciate your support to share this article with others to let the great Android and Kotlin community know. I’m just a beginner like you guys. I write what I learned. Please let me know what did I miss in this article.

Thank you for being here and keep reading…

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.¬†ūüôÉ