DEV Community

Cover image for Android notes: Understanding viewModelScope.launch{}
Tristan Elliott
Tristan Elliott

Posted on • Edited on

Android notes: Understanding viewModelScope.launch{}

Table of contents

  1. What we are talking about
  2. Main parts to a coroutine
  3. Background work
  4. Best practice

My app on the Google Playstore

GitHub code

Resources

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
}

Enter fullscreen mode Exit fullscreen mode
  • 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 use launch{} 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 to Dispatchers.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,
            )
        }

    }

Enter fullscreen mode Exit fullscreen mode
  • 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 use flowOn. 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,
            )
        }
    }

Enter fullscreen mode Exit fullscreen mode
  • Thanks to flowOn all the network requests are done on the Dispatchers.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,
            )
        }
    }

}

Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
ugurtekbas profile image
Ugur Tekbas

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.

Collapse
 
theplebdev profile image
Tristan Elliott
  • That actually sounds like a great idea!! So we move the dispatcher into the usecase? I would 100% implement this if my function had complex logic that I could also move. I don't really like to use UseCases unless I can put complex business logic inside of them.
  • Also, I could see you option be more viable if I(or anyone reading this) are part of a team. This does make for fewer moving parts.
  • Ok, I think your approach is correct for any real world scalable project. I think you would end up with a lot of extra UseCases. But that is not a bad thing and I think it would make the code a lot more explicit and any person reading the code could very easily see where the Dispatcher is changed.
Collapse
 
ugurtekbas profile image
Ugur Tekbas
  • Yes, I would specify the dispatcher in UseCase method so it's main-safe.
  • I believe we should always write code thinking of scalability and maintainability. (Unless we're trying to ship an MVP product with lighting fast :) )
  • Using UseCases is controversial topic in Android, personally I use them. You can check out advantages in this discussion.
Collapse
 
theplebdev profile image
Tristan Elliott
  • Also, if you know of any good Android learning resources, books, blogs, or even GitHub repos. Please let me know :)
Collapse
 
ugurtekbas profile image
Ugur Tekbas

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 :).

Collapse
 
ashwinn796 profile image
AshwinN796

Are you utilizing Retrofit for handling network calls? If so, is it necessary for us to switch the dispatcher?

Collapse
 
theplebdev profile image
Tristan Elliott

yes it is best practice to switch the dispatcher. Even if you are using Retrofit

Collapse
 
theplebdev profile image
Tristan Elliott • Edited

Here is the official best practices for coroutines in Android, HERE. Read about the injecting dispatchers section