Table of contents
My app on the Google Playstore
GitHub code
Resources
- Elements of Kotlin Coroutines by Mark L. Murphy
What I am trying to understand
- So inside my Android app I use a lot of code like this:
viewModelScope.launch{
// do some work here
}
- And up until now, I used it without any sort of real understanding of what is going on. So by the end of this blog post both you and I should have a better understanding of how this code works.
The 4 main parts to a coroutine.
- Within a coroutine there are 4 main parts
1) Scope
2) Builder
3) Dispatcher
4) Context
1) Scope
- All coroutine work is managed by a
coroutine scope
. Primarily a coroutine scope is responsible for canceling and cleaning up coroutines when the coroutine scope is no longer needed. With the previously mentioned code, the scope is viewModelScope. A viewModelScope is defined for each ViewModel in our app. Any coroutine launched in this scope is automatically canceled if the ViewModel is cleared. This is useful for any work that needs to be done only when the ViewModel is active. By destroying the scope when the ViewModel is destroyed, it saves us from potentially wasting resources and memory leaks. Thanks to viewModelScope all of our coroutine clean up and setup is done for us.
2) Builder
- All coroutines start with a coroutine builder. The block of code passed to the builder, along with anything called from that code block(directly or indirectly), represents the coroutine. For us the builder is
launch{}
, launch{} is a fire and forget coroutine builder. Which allows us to pass it a lambda expression to form the route of the coroutine. Since we don't have to wait for any sort of value we call it and forget about it
3) Dispatcher
- When using a coroutine builder, we can provide it a dispatcher. The dispatcher is what indicated what thread pool should be used for the executing the code inside the coroutine.
- We know with
viewModelScope.launch{}
there is a scope and a builder, but where is the dispatcher? As it turns out, when we uselaunch{}
without any parameters, it inherits the dispatcher from the coroutine scope. So that means that we inherit the dispatcher from viewModelScope. According to documentation the dispatcher of viewModelScope is hardcoded toDispatchers.Main
. Meaning, unless we provide a dispatcher to launch{}, all the work will be done on the main UI thread. Which is not good and we will see later on how we can avoid this.
4) Context
- This is the area of coroutines I am the most unfamiliar with, so I apologize for the brevity
- The dispatcher that we provide to a coroutine builder is always part of a CoroutineContext. As the name suggests the
CoroutineContext
provides a context for executing a coroutine.
Background work
- If you are like me and you are using
viewModelScope.launch{}
, there is a good chance you are using wrong. Before I started reading about coroutines my code looked like this:
fun getCalves() = viewModelScope.launch(){
getCalvesUseCase.execute(_uiState.value.calfLimit)
.collect{response ->
_uiState.value = _uiState.value.copy(
data = response,
)
}
}
- Which seems perfectly fine until you realize
getCalvesUseCase.execute
is making a network request and all that work is being done on the main UI thread and causing some Jank. Jank is defined as:
Android renders UI by generating a frame from your app and displaying it on the screen. If your app suffers from slow UI rendering, then the system is forced to skip frames. When this happens, the user perceives a recurring flicker on their screen, which is referred to as jank.
How can we fix some Jank?
You may have noticed from the code block above that I am using
.collect{}
which means I am using Flows. So now we have to figure out how to change dispatchers with the Flow api. This is pretty easy thanks to flowOn. To change the dispatcher of a flow we can useflowOn
. This will make the producer of the flow(code that calls emit) work on the background thread and the upstream of the flow will be unaffected. The new and approved code looks like this:
fun getCalves() = viewModelScope.launch(){
getCalvesUseCase.execute(_uiState.value.calfLimit)
.flowOn(Dispatchers.IO) //network requests done on
//background thread
.collect{response -> // done on main thread
_uiState.value = _uiState.value.copy(
data = response,
)
}
}
- Thanks to
flowOn
all the network requests are done on theDispatchers.IO
which is specifically optimized to handle network requests.
Best Practice
- It is considered best practice to inject dispatchers into your ViewModel:
class MainViewModel constructor(
private val dispatcherIO: CoroutineDispatcher = Dispatchers.IO
):ViewModel() {
fun getCalves() = viewModelScope.launch(){
getCalvesUseCase.execute(_uiState.value.calfLimit)
.flowOn(dispatcherIO) //network requests done on
//background thread
.collect{response -> // done on main thread
_uiState.value = _uiState.value.copy(
data = response,
)
}
}
}
Conclusion
- Thank you for taking the time out of your day to read this blog post of mine. If you have any questions or concerns please comment below or reach out to me on Twitter.
Top comments (8)
This is a great series Tristan, thanks a lot. I wish we have more posts about things we use and don't know what they do in dept.
While your
viewModelScope.launch{...}
using approach is absolutely fine I would like to point out a different way of making api calls main safe. Which means; calling such functions from main thread wouldn't be problem and care free.For this purpose we change the context of the coroutine in use case method and specify a different Dispatcher like below.
class GetCalvesUseCase @Inject constructor(
private val dataSource: DataSource
){
suspend fun getCalveList(): Flow<List<Calve>> {
return withContext(Dispatchers.IO) {
dataSource.getCalvesFromAPI()
}
}
}
And after this change, ViewModel can call this method safely, without caring to switch dispatchers and also don't need to inject them.
getCalvesUseCase. getCalveList()
.collect{ response ->
_uiState.value = _uiState.value.copy(
data = response,
)
}
}
Let me know what you think.
I usually read blogs of companies who have established Android teams and big, successful products, such as: Github, Uber, Spotify, Airbnb.
Also read a lot of official documentation and posts from Google, you can join communities in Twitter and follow me if you like for more interesting Android topics :).
Are you utilizing Retrofit for handling network calls? If so, is it necessary for us to switch the dispatcher?
yes it is best practice to switch the dispatcher. Even if you are using Retrofit
Here is the official best practices for coroutines in Android, HERE. Read about the injecting dispatchers section