Question

Updating MutableLiveData from Coroutines in Kotlin: setValue vs postValue

kotlinandroid-livedatakotlin-coroutines

Question

I am trying to update a LiveData value from inside a coroutine:

object AddressList : MutableLiveData<List<Address>>()

fun getAddressesLiveData(): LiveData<List<Address>> {
    AddressList.value = listOf()

    GlobalScope.launch {
        AddressList.value = getAddressList()
    }

    return AddressList
}

However, I get this error:

IllegalStateException: Cannot invoke setValue on a background thread

How can MutableLiveData be updated correctly when using Kotlin coroutines?

Short Answer

By the end of this page, you will understand why MutableLiveData.value can only be set on the main thread, how postValue() differs from setValue(), and how to use coroutines safely with LiveData in Android. You will also learn better coroutine patterns than GlobalScope.launch for real projects.

Concept

MutableLiveData is designed to hold UI-related state in Android. Because UI state is closely tied to the main thread, LiveData enforces a simple rule:

  • Use value / setValue() on the main thread
  • Use postValue() from a background thread

In your example, GlobalScope.launch does not guarantee execution on the Android main thread in a way that makes your value assignment safe for all cases. When the coroutine runs on a background thread and you call:

AddressList.value = getAddressList()

that internally calls setValue(), which throws this exception:

IllegalStateException: Cannot invoke setValue on a background thread

This matters because Android UI code must be thread-safe. If multiple threads could directly change UI-observed state without coordination, apps would become unpredictable and crash more often.

The usual solutions are:

  • call postValue() from a background thread
  • switch to Dispatchers.Main before assigning to value
  • avoid GlobalScope and use a lifecycle-aware scope such as viewModelScope

In short, coroutines work perfectly with LiveData, but you must update it on the correct thread.

Mental Model

Think of MutableLiveData like a notice board in a school office.

  • setValue() is like walking into the office and pinning a note directly onto the board. You can only do that when the office is open and staff are present — that is the main thread.
  • postValue() is like dropping the note into a mailbox. Staff will later take it out and pin it to the board safely — that is a background thread posting work to the main thread.

So if your coroutine is working in the background, you should not walk straight to the board. You should either use the mailbox (postValue) or return to the office (Dispatchers.Main) first.

Take Quiz

Syntax and Examples

The two most important ways to update MutableLiveData are:

liveData.value = newValue      // main thread only
liveData.postValue(newValue)   // safe from background thread

Example 1: Using postValue() from a coroutine

val addressList = MutableLiveData<List<Address>>()

fun getAddressesLiveData(): LiveData<List<Address>> {
    addressList.value = emptyList()

    GlobalScope.launch(Dispatchers.IO) {
        val result = getAddressList()
        addressList.postValue(result)
    }

    return addressList
}

Here:

  • Dispatchers.IO is used for background work
  • getAddressList() runs off the main thread
  • postValue(result) safely schedules the update

Example 2: Switching back to the main thread

val addressList = MutableLiveData<List<Address>>()

fun getAddressesLiveData(): LiveData<List<Address>> {
    addressList.value = emptyList()

    GlobalScope.launch(Dispatchers.IO) {
        val result = getAddressList()
        withContext(Dispatchers.Main) {
            addressList.value = result
        }
    }

    return addressList
}

This version does the fetch in the background, then switches to the main thread before using value.

Recommended ViewModel pattern

In real Android apps, prefer viewModelScope instead of GlobalScope:

class AddressViewModel : ViewModel() {
    private val _addresses = MutableLiveData<List<Address>>()
    val addresses: LiveData<List<Address>> = _addresses

    fun loadAddresses() {
        _addresses.value = emptyList()

        viewModelScope.launch {
            val result = withContext(Dispatchers.IO) {
                getAddressList()
            }
            _addresses.value = result
        }
    }
}

This is safer because the coroutine is cancelled automatically when the ViewModel is cleared.

Step by Step Execution

Consider this example:

val users = MutableLiveData<List<String>>()

fun loadUsers() {
    users.value = emptyList()

    GlobalScope.launch(Dispatchers.IO) {
        val result = listOf("Ana", "Ben", "Chris")
        users.postValue(result)
    }
}

Step by step:

  1. users.value = emptyList() runs on the current thread.

    • If called from the UI layer, this is usually the main thread.
    • users now contains an empty list.
  2. GlobalScope.launch(Dispatchers.IO) starts a coroutine on an IO/background thread.

    • This is suitable for network or database work.
  3. val result = listOf("Ana", "Ben", "Chris")

    • The background work completes and produces data.
  4. users.postValue(result) is called.

    • Because this is a background thread, postValue() is the safe choice.
    • Android schedules the LiveData update to happen on the main thread.
  5. Observers of users receive the new list.

    • The UI can now render the updated data.

If you had written this instead:

users.value = result

inside the IO coroutine, it would fail because value uses setValue(), which must run on the main thread.

Real World Use Cases

This pattern appears often in Android apps:

  • API calls: fetch data in Dispatchers.IO, then update LiveData
  • Database queries: read from Room or another database in the background and publish results
  • File loading: parse files or JSON off the main thread, then notify the UI
  • Form validation: compute validation results, then expose them to the screen
  • Progress updates: background work posts loading, success, or error state

Example with loading state:

data class UiState(
    val loading: Boolean,
    val addresses: List<Address> = emptyList(),
    val error: String? = null
)

A ViewModel may update this state before, during, and after a request. The key idea stays the same: background work should not directly call setValue() unless you first switch back to the main thread.

Take Quiz

Real Codebase Usage

In real projects, developers rarely expose a mutable LiveData directly. A common pattern is:

private val _data = MutableLiveData<List<Address>>()
val data: LiveData<List<Address>> = _data

This prevents outside classes from modifying the value.

Common patterns

1. Early loading state

_addresses.value = emptyList()

or a dedicated loading flag:

_loading.value = true

2. Background fetch + main-thread update

viewModelScope.launch {
    try {
        val result = withContext(Dispatchers.IO) {
            repository.getAddressList()
        }
        _addresses.value = result
    } catch (e: Exception) {
        _error.value = e.message
    }
}

3. Error handling

Coroutines often wrap repository calls in try/catch so the UI can observe failures safely.

4. Repository separation

A repository usually fetches data, while the ViewModel updates LiveData:

class AddressRepository {
    suspend fun getAddressList(): List<Address> {
        return listOf()
    }
}

This keeps UI state logic out of data-fetching code.

5. Avoiding GlobalScope

GlobalScope is generally avoided because:

  • it is not lifecycle-aware
  • it can outlive the screen
  • it may continue running after the UI is gone
  • it makes cancellation harder

viewModelScope is usually the better choice in Android architecture.

Common Mistakes

1. Using value from a background thread

Broken code:

GlobalScope.launch(Dispatchers.IO) {
    liveData.value = fetchData()
}

Why it fails:

  • value calls setValue()
  • setValue() must run on the main thread

Fix:

GlobalScope.launch(Dispatchers.IO) {
    liveData.postValue(fetchData())
}

or:

GlobalScope.launch(Dispatchers.IO) {
    val result = fetchData()
    withContext(Dispatchers.Main) {
        liveData.value = result
    }
}

2. Using GlobalScope for UI work

Broken idea:

GlobalScope.launch {
    // UI-related async work
}

Why it is risky:

  • it is not tied to Activity or ViewModel lifecycle
  • it may leak work or update dead screens

Better:

viewModelScope.launch {
    // lifecycle-aware coroutine
}

3. Exposing MutableLiveData publicly

Broken pattern:

val addresses = MutableLiveData<List<Address>>()

Why it is risky:

  • any class can modify it

Better:

private val _addresses = MutableLiveData<List<Address>>()
val addresses: LiveData<List<Address>> = _addresses

4. Doing heavy work on the main thread

Broken code:

viewModelScope.launch {
    val result = getAddressList() // slow network/database call on main thread
    _addresses.value = result
}

Better:

viewModelScope.launch {
    val result = withContext(Dispatchers.IO) {
        getAddressList()
    }
    _addresses.value = result
}

5. Confusing postValue() timing

postValue() does not update immediately. It schedules the update. If you need an immediate update and you are already on the main thread, use value instead.

Comparisons

ConceptWhen to useThread ruleNotes
liveData.value = xWhen already on the main threadMain thread onlyImmediate LiveData update
liveData.setValue(x)Same as value = xMain thread onlyvalue is property syntax for this
liveData.postValue(x)When on a background threadAny threadPosts update to main thread later
withContext(Dispatchers.Main) { liveData.value = x }When you want to explicitly switch to mainSafe after switchingUseful in coroutines

postValue() vs switching to main thread

ApproachAdvantageTrade-off
postValue()Simple and safe from background threadsUpdate is asynchronous
withContext(Dispatchers.Main) + valueClear control over thread and timingSlightly more code

GlobalScope vs viewModelScope

ScopeBest forLifecycle-awareRecommended for Android UI?
GlobalScopeRare app-wide tasksNoUsually no
viewModelScopeViewModel workYesYes
Take Quiz

Cheat Sheet

liveData.value = data      // main thread only
liveData.postValue(data)   // background thread safe

Rules

  • value and setValue() must run on the main thread
  • postValue() can be called from a background thread
  • Use Dispatchers.IO for blocking work
  • Use Dispatchers.Main for UI state updates
  • Prefer viewModelScope over GlobalScope in Android UI code

Common coroutine pattern

viewModelScope.launch {
    val result = withContext(Dispatchers.IO) {
        repository.loadData()
    }
    _liveData.value = result
}

Background-thread alternative

viewModelScope.launch(Dispatchers.IO) {
    val result = repository.loadData()
    _liveData.postValue(result)
}

Good encapsulation

private val _items = MutableLiveData<List<Item>>()
val items: LiveData<List<Item>> = _items

Edge case

  • postValue() is asynchronous, so the new value may not be available immediately after the call.

FAQ

Why does MutableLiveData.value crash inside a coroutine?

A coroutine can run on different threads. If it is running on a background thread, assigning to value calls setValue(), which is only allowed on the main thread.

Should I use postValue() or value with coroutines?

Use postValue() if you are on a background thread. Use value if you are already on the main thread or after switching with withContext(Dispatchers.Main).

Is GlobalScope.launch a good idea for LiveData updates?

Usually no. In Android UI code, viewModelScope is preferred because it is lifecycle-aware and cancels work automatically when the ViewModel is destroyed.

Can I call postValue() on the main thread?

Yes, but it is usually unnecessary. If you are already on the main thread, value is often clearer.

What is the difference between value and setValue()?

They are effectively the same for LiveData. value = x is Kotlin property syntax that calls setValue(x).

What dispatcher should I use for network or database work?

Use Dispatchers.IO for blocking tasks like network requests, file access, or database operations.

Can a repository return LiveData directly?

It can, but a common pattern is for repositories to return plain data or suspend results, while the ViewModel exposes LiveData to the UI.

Take Quiz

Mini Project

Description

Build a small ViewModel that loads a list of addresses in the background and exposes them to the UI through LiveData. This demonstrates the correct way to combine coroutines, background work, and main-thread-safe UI state updates.

Goal

Create a lifecycle-aware ViewModel that fetches address data asynchronously and updates LiveData without triggering thread errors.

Requirements

  1. Create a ViewModel with a private MutableLiveData<List<Address>> and a public LiveData<List<Address>>.
  2. Add a function that loads addresses asynchronously.
  3. Perform the data fetch on Dispatchers.IO.
  4. Update the LiveData safely when the result is ready.
  5. Handle errors by exposing an error message through another LiveData.
Take Quiz

Keep learning

Related questions

View more