64

While I was learning coroutines and how to properly use them in an android app I found something I was surprised about.

When launching a coroutine using viewModelScope.launch { } and setting a breakpoint inside the launch lambda I noticed my app wasn't responsive anymore because it was still on the main thread.

This confuses me because the docs of viewModelScope.launch { } clearly state:

Launches a new coroutine without blocking the current thread

Isn't the current thread the main thread ? What is the whole purpose of launch if it doesn't run on a different thread by default ?

I was able to run it on anther thread using viewModelScope.launch(Dispatchers.IO){ } which works as I was expecting, namely on another thread.

What I am trying to accomplish from the launch method is to call a repository and do some IO work namely call a webservice and store the data in a room db. So I was thinking of calling viewModelScope.launch(Dispatchers.IO){ } do all the work on a different thread and in the end update the LiveData result.

viewModelScope.launch(Dispatchers.IO){ liveData.postValue(someRepository.someWork()) }

So my second question is, is this the way to go ?

Jonas Geiregat
  • 5,214
  • 4
  • 41
  • 60
  • 7
    Thanks to this question, and after a year of assuming `viewModelScope.launch` would be enough to move the work off Main, I've updated my method to run the function body wrapped in `withContext(Dispatchers.IO)` and suddenly my app performance skyrocketed! – rmirabelle May 19 '21 at 20:47

4 Answers4

36

ViewModelScope.launch { } runs on the main thread, but also gives you the option to run other dispatchers, so you can have UI & Background operations running synchronously.

For you example:

fun thisWillRunOnMainThread() {

    viewModelScope.launch { 

        //below code will run on UI thread.
        showLoadingOnUI()

        //using withContext() you can run a block of code on different dispatcher
        val result = novel.id = withContext(Dispatchers.IO) {
            withsomeRepository.someWork()
        }

        //The below code waits until the above block is executed and the result is set.
        liveData.value = result
        finishLoadingOnUI()
    }
}

For more reference, I would say there are some neat articles that will help you understand this concept.

Medium link that explains it really neat.

ngoa
  • 720
  • 4
  • 14
  • 1
    This is how my code has been written but after the needed value has been saved I am unable to display the data because control is not going to activity from the ViewModel. Stuck with this for weeks – Meenohara Jul 30 '21 at 13:27
16

So my second question is, is this the way to go ?

I would expect two things to be different in your current approach.

1.) First step would be to define the scheduler of the background operation via withContext.

class SomeRepository {
    suspend fun doWork(): SomeResult = withContext(Dispatchers.IO) {
        ...
    }
}

This way, the operation itself runs on a background thread, but you didn't force your original scope to be "off-thread".

2.) Jetpack Lifecycle KTX provides the liveData { coroutine builder so that you don't have to postValue to it manually.

val liveData: LiveData<SomeResult> = liveData {
    emit(someRepository.someWork())
}

Which in a ViewModel, you would use like so:

val liveData: LiveData<SomeResult> = liveData(context = viewModelScope.coroutineContext) {
    withContext(Dispatchers.IO) {
        emit(someRepository.someWork())
    }
}

And now you can automatically trigger data-loading via observing, and not having to manually invoke viewModelScope.launch {}.

EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
  • viewModelScope.launch{ } only runs on first call. second it doesn't call – Nitish Feb 15 '21 at 13:02
  • 1
    @Nitish not sure what you are referring to exactly, do you mean how when you navigate to the screen, then `liveData {` becomes active and executes the block? – EpicPandaForce Feb 15 '21 at 13:04
  • Thanks for quick response. actually i am calling a function in onResume of fragment. In that function i am getting some data from room DB. In that function viewModelScope.launch { withContext(Dispachers.IO){ db.cbDao().getAll() } }. but my debugger is not going inside the launch scope. – Nitish Feb 15 '21 at 13:16
  • yeah the debugger isn't very good when it comes to coroutines or especially flows – EpicPandaForce Feb 15 '21 at 13:50
  • @EpicPandaForce by my understanding, if you're using ViewModel, should use viewModelScope, can't just use liveData builder to replace it, please refer detail in this post https://stackoverflow.com/questions/57698932/livedatascope-vs-viewmodelscope-in-android – Amos Sep 13 '21 at 01:57
  • 1
    @Amos you are right, `viewModelScope.coroutineContext` should be passed to `liveData {`. Updated. – EpicPandaForce Sep 13 '21 at 11:19
4

The idea behind main thread being default is you can run UI operations without having to change the context. It is a convention I guess Kotlin coroutine library writers have chosen

Suppose if by default if the launch runs on IO thread then the code would look like this

viewmodelScope.launch {
  val response = networkRequest() 
  withContext(Dispatchers.Main) {
    renderUI(response)
  } 
}

Suppose if by default if the launch runs on Default thread then the code would look like this

viewmodelScope.launch {
  val response: Response = null
  withContext(Dispatchers.IO) {
     response = networkRequest()
  }
  withContext(Dispatchers.Main) {
    renderUI(response)
  } 
}

Since the default launch is on main thread, now you have to do below

viewmodelScope.launch {
  val response: Response = null
  withContext(Dispatchers.IO) {
     response = networkRequest()
  }
  renderUI(response)
}

To avoid the messy code initializing the response with null, we can also make the networkRequest as suspend and wrap the business logic of networkRequest() function in withContext(Dispatchers.IO) and that's how lot of people write their networkRequest() function as well! Hope this makes sense

murali kurapati
  • 1,510
  • 18
  • 23
1

One of the main reasons it runs on Main thread, is because it's more practical for general use in ViewModel, like murali kurapati wrote. It was a design choice.

It's also important to note that all suspending functions should be "main safe" according to best pracices. That means, that your repository should switch coroutine context accordingly, like so:

class someRepository(private val ioDispatcher: CoroutineDispatcher) {
    suspend fun someWork() {
        withContext(ioDispatcher) {
            TODO("Heavy lifting")
        }
    }
}
Peter
  • 340
  • 4
  • 13
  • How can it be practical? if the default was .IO then there would not even be a need for specifying ioDispatcher in the repositories. – George Shalvashvili Aug 30 '22 at 19:48
  • From architectural POV, repository should be the one responsible in which coroutine context it should run. Moving this to VM gives all of the VMs using that repository additional responsibility. And also note, that not everything should run on IO dispatcher. That's my thinking about it. I suggest you read [this](https://medium.com/@tfcporciuncula/the-fact-that-viewmodelscope-operates-on-the-main-thread-is-the-reason-why-you-dont-have-to-call-f3864fd8df1d) article (and links inside), where they exactly address your argument. – Peter Aug 31 '22 at 20:56