0

I am using OkHttp library to download some data from the internet in my androidx.lifecycle.ViewModel I then want to update my LiveData. It seems that doing it from background thread throws exception like so:

2022-01-17 15:47:59.589 7354-7396/com.example.myapplication E/AndroidRuntime: FATAL EXCEPTION: OkHttp Dispatcher
    Process: com.example.myapplication, PID: 7354
    java.lang.IllegalStateException: Cannot invoke setValue on a background thread
        at androidx.lifecycle.LiveData.assertMainThread(LiveData.java:487)
        at androidx.lifecycle.LiveData.setValue(LiveData.java:306)
        at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java:50)
        at com.example.myapplication.MainActivityViewModel$getOneMoreCat$1.invoke(MainActivityViewModel.kt:86)
        at com.example.myapplication.MainActivityViewModel$getOneMoreCat$1.invoke(MainActivityViewModel.kt:39)
        at com.example.myapplication.singleton.CommunicationManager$sendRequest$1.onResponse(CommunicationManager.kt:24)
        at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:923)

Now I found two different ways to dispatch to main thread from ViewModel (which has no reference to Context as per AAC guidelines), see here:

            GlobalScope.launch {
                withContext(Dispatchers.Main) {
                    // do whatever, e.g. update LiveData
                }
            }

or

            Handler(Looper.getMainLooper()).post(Runnable {
                   // do whatever, e.g. update LiveData
            })

Which is the correct way? That is, least impactful at runtime.

Update I did find that I can also do myLiveData.post() and it works from background thread.

Still, I'd like to know what is the correct way to dispatch work to main thread in modern Android under kotlin

Sergio
  • 27,326
  • 8
  • 128
  • 149
zaitsman
  • 8,984
  • 6
  • 47
  • 79
  • 1
    If its just setting value to `LiveData` you can just use `postValue`. If there is any othere UI operation then you can use `Dispatchers` . Apart from this do not use `GlobalScope`. See [This](https://stackoverflow.com/questions/54335365/why-not-use-globalscope-launch) and [This](https://stackoverflow.com/questions/65008486/globalscope-vs-coroutinescope-vs-lifecyclescope). – ADM Jan 17 '22 at 05:21

3 Answers3

4

The right way to dispatch work from Background Thread to Main Thread using LivaData is to use LivaData.postValue() method. It posts a task to a main thread to set the given value.

Another approach is to use viewModelScope extension property in ViewModel class, by default it uses Dispatchers.Main context to execute a coroutine, it means you can update UI in such coroutine. For example, in your ViewModel class:

viewModelScope.launch {
    val result = makeNetworkCall()
    // use result to update UI
    liveData.value = result
}

// withContext - switches context to background thread
suspend fun makeNetworkCall(): String = withContext(Dispatchers.IO) {
    delay(1000) // simulate network call
    "SomeResult"
}

Dependency to use viewModelScope:

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'

GlobalScope is highly discouraged to use, it can only be used in specific cases, here is a description why not use it.

Sergio
  • 27,326
  • 8
  • 128
  • 149
0

Inside viewmodel,

private val _downloading = MutableLiveData<Result<Boolean>>()
val downloading: LiveData<Result<Boolean>>
    get() = _downloading

fun downloadFile() {
    viewModelScope.launch {
        try {
            _downloading.value = Result.Loading
            val result = withContext(Dispatchers.IO) {
                // download something
            }
            _downloading.value = Result.Success(true)
        } catch (ex: Exception) {
            _downloading.value = Result.Failure(ex)
        }
    }
}

In activity/fragment,

 viewModel.downloading.observe(this, {
        when (it) {
            is Result.Failure -> TODO()
            Result.Loading -> TODO()
            is Result.Success -> TODO()
        }
    })

Result is a sealed class to capture state, which in turn will help us update the UI accordingly. Also viewmodelscope is used instead of GlobalScope since we don't want the download to go on when the viewmodel is destroyed.

Hussain
  • 1,243
  • 12
  • 21
-1

there are many ways to do that you can simply post value to live data, using dispatcher's and handler which is running on main thread as you provide looper of main thread.

Another way is you can use high order functions to update the viewmodels which is easy to use and give it a try.

  • High order functions will leak because they would capture context and networking can persist activity recreation (e.g. screen rotated) – zaitsman Jan 17 '22 at 06:02