Reputation: 717
I've been going through this codelab to learn about coroutines. One thing that still isn't clear to me is that why do we need to change dispatchers to ensure that we don't block the main/UI thread? If coroutines are light-weight threads, then why can't I invoke thread-blocking functions (whether they're suspending or not) within a coroutine when I'm already on the main thread?
The codelab explains that (in summary) if I write this code:
// Repository.kt
suspend fun repoRefreshTitle() {
delay(500)
}
//ViewModel.kt
fun vmRefreshTitle() {
viewModelScope.launch {
_spinner.value = true
repository.repoRefreshTitle()
}
}
...then this won't block the main thread. delay()
is a suspend
function, so the coroutine created by viewmodelScope.launch
will be paused until the 500ms passes. The main thread won't be blocked though.
However, if I refactor repoRefreshTitle()
to the following:
suspend fun repoRefreshTitle() {
val result = nonSuspendingNetworkCall()
}
...then that network call will actually be done on the main thread. Is that correct? I would have to change to another dispatcher to offload the work to an IO thread:
suspend fun repoRefreshTitle() {
withContext(Dispatchers.IO) {
val result = nonSuspendingNetworkCall()
}
}
I must be oversimplifying this somehow. Isn't the fact that I'm already in a coroutine enough? Why do I have to switch the dispatcher?
Upvotes: 5
Views: 6200
Reputation: 1791
See this documents which describes that the Dispatchers.IO
is specially designed for I/O
operations where
viewModelScope.launch {
}
creates a coroutine block that is lifecycle
aware and suitable for any asynchronous operation not specialized for I/O
operation. When your ViewModel
is destroyed then
viewModelScope.launch{
// Invoke network suspend functions from repository
// Or any kind of asynchronous operation
}
will be stopped and cancelled which will cancel this block
withContext(Dispatchers.IO) {
// Invoke only I/O operations
}
too because viewModelScope
keeps communication with that withContext(Dispatchers.IO)
.
You should not make your viewModelScope
busy with I/O
operations instead you should let another I/O specialized coroutine thread maintain that I/O operation and keep track from viewModelScope
. This will make viewModelScope
more light weight.
Upvotes: 0
Reputation: 2859
When you run your code inside viewModelScope
it doesn't mean your main thread won't freeze. It just ensures if you started work on MainThread and you are waiting for another thread to return a result, it won't block the main thread, e.g. calling an API with Retrofit and waiting to update LiveData in your ViewModel.
So Why do you need to change Coroutine Scope? (probably using withContext
)
You start your work on Main Thread and switch to another coroutine for heavy work and easily get the result back on the main thread when the result is ready.
fun onSaveImageFile(source: Int, filename: String) = viewModelScope.launch {
val isFileSaved = withContext(Dispatchers.IO) {
FileRepository.saveImageFile(source, filename)
}
toastViewModel.postValue(if (isFileSaved) "Image file saved!" else "Failed to save image file!")
}
Upvotes: 3
Reputation: 1006614
The codelab explains that (in summary) if I write this code...then this won't block the main thread. delay() is a suspend function, so the coroutine created by viewmodelScope.launch will be paused until the 500ms passes. The main thread won't be blocked though.
Correct. However, what little real "work" there is in delay()
will be performed on the main application thread, because the default dispatcher for viewModelScope.launch()
is based on Dispatchers.Main
.
However, if I refactor repoRefreshTitle() to the following...then that network call will actually be done on the main thread. Is that correct?
Correct. nonSuspendingNetworkCall()
, like delay()
, will be run on the main application thread. In the nonSuspendingNetworkCall()
, that's not a good thing.
I would have to change to another dispatcher to offload the work to an IO thread
Correct. More specifically, you need to use a dispatcher that uses a background thread. For I/O, Dispatchers.IO
is a common choice.
Isn't the fact that I'm already in a coroutine enough? Why do I have to switch the dispatcher?
Because we do not want to do network I/O on the main application thread. Dispatchers.Main
runs its coroutines on the main application thread, and that is the default dispatcher for viewModelScope.launch()
. That's one of the reasons why, in a lot of what I write, I specifically write viewModelScope.launch(Dispatchers.Main)
— that's more wordy (and technically slightly different than the default), but it is more obvious to readers.
Upvotes: 5