RxJava Vs Coroutines
When we work on Android projects, all of us need to do API calls, and at times we need to handle multiple network calls. The requirement might be to execute the asynchronous calls chained or in parallel, and sometimes even the result of these API calls need to be combined.
Most of us have gotten it under control with RxJava. But we probably don’t really have full comprehension of how RxJava works, or all that it can do.
You probably already know what Retrofit is and most likely you’ve been using it in conjunction with RxJava to handle asynchronous and concurrent network requests, but what if I told you there is an alternative and easier way?
We have our production app, which uses RxJava extensively. It works quite fine. But it has some limitations. I hoped Kotlin coroutines could solve some of these problems. So I did a POC for coroutines. So here I am gonna share some of my learning.
First things first, why to move from RxJava?
- each chain of calls usually ends with a couple of callbacks set in the subscribe() method
- it’s hard to create modular code that can be composed in more complex flows and still keep it readable (this makes it hard to adapt to changing requirements of the app)
- due to the fluent API, the flow is hard to read and change when it’s very complex
- we usually never have to deal with real streams while getting results from a REST API because we usually simply deal with collections received as JSON objects, so using RxJava is not strictly necessary, especially given what Kotlin offers out of the box
- The project size increases as the method counts increases
Coroutines are super light weight. Coroutines simplifies the asynchronous code. It makes the asynchronous code look almost similar to synchronous code. Which brings in a lot of readability. Let us see how to use coroutines with Retrofit.
Retrofit + Coroutines -> Networking made easy
From the retrofit 2.6.0 version, it supports suspend functions. So we will see how to use suspend functions to make network calls.
Suspend functions
Suspend function is a function that could be started, paused, and resume. These functions are only allowed to be called from a coroutine or another suspend function.
Retrofit API Interface
interface APIService {
@GET("users")
suspend fun getUsers():Users @GET("userDetails")
suspend fun getUserDetails(@Query("id") id:String):UserDetails
}
RemoteRepository
//getApiService() will return the Retrofit API Interfaceoverride suspend fun getUsers(): ResponseState<Users> {
return getResponse("Get_Users",suspend {
getApiService()?.getUsers()
})
}
override suspend fun getUserDetails(id: String): ResponseState<UserDetails> {
return getResponse("Get_User_Details",suspend {
getApiService()?.getUserDetails(id)
})
}suspend fun <T : BaseModel> getResponse(tag:String,backgroundBlock:suspend () -> T?): ResponseState<T> {
try {
var data: T? = null
data = backgroundBlock()
data?.let {
return ResponseState.ResponseSuccess(it)
} ?: run {
return ResponseState.ResponseError("Null Error",Throwable())
}
} catch (e: Exception) {
//handle exception
return ResponseState.ResponseError("Error", Throwable()) }
}sealed class ResponseState<out T> {
class ResponseLoading<out T>(tag: String) : ResponseState<T>() {
override fun toString(): String {
return "Response Loading :: tag"
}
}
data class ResponseSuccess<out T>(val data: T) : ResponseState<T>()
data class ResponseError<out T>(val message: String, val throwable: Throwable) :
ResponseState<T>()
}
ViewModel
We can make the API calls sequential or parallel according to our need. also we can combine or manipulate the results in viewmodel.
class MainViewModel(application: Application) : BaseViewModel(application) {//getting users
fun getUsers():MutableLiveData<ResponseState<Users>>{
var liveData=MutableLiveData<ResponseState<Users>>()
liveData.value = ResponseState.ResponseLoading("Loading Users")
viewModelScope.launch {
val users = apiDataSource.getUsers()
//Manipulate the result if needed
...
liveData.value = users
}
return liveData
}//get users and get details of the first user. Here we use the result of getUsers() to get the id of the first user, thus the api calls are actually sequential.
fun getUserDetails():MutableLiveData<ResponseState<UserDetails>>{
var liveData=MutableLiveData<ResponseState<UserDetails>>()
liveData.value=ResponseState.ResponseLoading("Loading UserDetails")
viewModelScope.launch {
val response=apiDataSource.getUsers()
if(response is ResponseState.ResponseSuccess){
val firstUserId=response.data.users[0]?.id
liveData.value=apiDataSource.getUserDetails(firstUserId)
}
}
return liveData
}
}
Overall, I’m impressed by how coroutine helps us simplify asynchronous programming. Moreover, It gets us out of the callback hell and makes it easier to handle complex network requests. I think there is no excuse not to give coroutine a try now.
As we have come to the conclusion, whether to use coroutines or RxJava, use this algorithm
- if you are already using RxJava and it works for you, then use RxJava
- if the architecture is based on reactive stream, then use RxJava
- if the project is multiplatform with Kotlin Native, then use coroutines
- if the codebase is Java, then use RxJava
- else, use coroutines
On an additional note, with Flow in Kotlin now you can handle a stream of data that emits values sequentially. I will write about Kotlin Flows in the next article.