Skip to content
intermediate Phase 5 · Jetpack Libraries

LiveData

Use LiveData for observable, lifecycle-aware data holders. Understand setValue vs postValue.

40m
2 problems
Topic Progress 0%

LiveData Basics

What Is LiveData?

LiveData is an observable data holder that is lifecycle-aware. When a LiveData value changes, observers with STARTED or RESUMED state receive updates automatically. Observers in DESTROYED state are removed, preventing memory leaks.

This eliminates the common bug in Android where background threads update UI after the Activity is gone.

Creating LiveData

class UserViewModel(private val repo: UserRepository) : ViewModel() {
    private val _userName = MutableLiveData<String>()
    val userName: LiveData<String> = _userName

    fun loadUser(id: String) {
        viewModelScope.launch {
            val user = repo.getUser(id)
            _userName.value = user.name  // setValue: main thread only
        }
    }
}

Expose LiveData<String> (immutable) to the UI, keep MutableLiveData<String> private in the ViewModel.

Observing in Activities

class UserActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val viewModel: UserViewModel by viewModels()

        // Observer is automatically removed when Activity is destroyed
        viewModel.userName.observe(this) { name ->
            findViewById<TextView>(R.id.nameText).text = name
        }
    }
}

Observing in Compose

Use observeAsState() from androidx.compose.runtime:lifecycle-runtime-compose:

@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
    val name by viewModel.userName.observeAsState("")
    Text(text = "Name: $name", style = MaterialTheme.typography.headlineMedium)
}

setValue vs postValue

setValue(value) must be called on the main thread. It updates the value synchronously and notifies observers immediately.

postValue(value) can be called from any thread. It posts the update to the main thread, so observers receive it asynchronously. Only the last posted value wins if multiple posts happen before the main thread processes them.

// Main thread — use setValue
_userName.value = "Alice"

// Background thread — use postValue
repository.fetchUser { user ->
    _userName.postValue(user.name)  // Queued for main thread
}

Rule of thumb: prefer setValue from viewModelScope.launch(Dispatchers.Main) and only use postValue when you genuinely cannot post to the main dispatcher.

Transformations and MediatorLiveData

Transformations.map

map transforms the value inside a LiveData without changing the source. It creates a new LiveData that applies a function to each emission.

val userLiveData: LiveData<User> = repo.getUserFlow().asLiveData()

// Transform User to just the name
val userName: LiveData<String> = userLiveData.map { user ->
    user.name
}

The transformation is lazy — it only runs when there is an active observer. No computation happens if nothing is listening.

Transformations.switchMap

switchMap is for when each value in a source LiveData should produce a different LiveData. It is commonly used for database queries or API calls that depend on a parameter.

class SearchViewModel(private val repo: SearchRepository) : ViewModel() {
    private val _query = MutableLiveData<String>()
    val query: LiveData<String> = _query

    val searchResults: LiveData<List<Result>> = _query.switchMap { query ->
        repo.searchResults(query).asLiveData()
    }

    fun search(newQuery: String) {
        _query.value = newQuery
    }
}

When query changes, switchMap cancels the previous LiveData and subscribes to the new one. This prevents stale results.

MediatorLiveData

MediatorLiveData observes multiple LiveData sources and merges them into one:

class FormViewModel : ViewModel() {
    val email = MutableLiveData<String>()
    val password = MutableLiveData<String>()

    val isFormValid: LiveData<Boolean> = MediatorLiveData<Boolean>().apply {
        fun update() {
            val emailValid = Patterns.EMAIL_ADDRESS.matcher(email.value ?: "").matches()
            val passwordValid = (password.value?.length ?: 0) >= 8
            value = emailValid && passwordValid
        }
        addSource(email) { update() }
        addSource(password) { update() }
    }
}

LiveData vs StateFlow

Feature LiveData StateFlow
Lifecycle awareness Built-in Requires collectAsStateWithLifecycle
Transformation map, switchMap map, flatMapLatest
Main thread Required for setValue Any thread via value setter
Compose integration observeAsState() Native with collectAsState
Coroutines support asLiveData() extension Native Flow

LiveData is still valid in existing codebases. For new projects, StateFlow with Compose is preferred.

Quiz

1. What happens to a LiveData observer when the Activity enters the DESTROYED state?

Question 1 options

2. When should you use postValue instead of setValue?

Question 2 options

3. What does Transformations.switchMap do when the source LiveData emits a new value?

Question 3 options

4. Why is LiveData exposed as LiveData<T> instead of MutableLiveData<T>?

Question 4 options

Flashcards

Question

What is the difference between setValue and postValue in LiveData?

Answer

setValue must be called on the main thread and updates synchronously. postValue can be called from any thread and posts the update to the main thread asynchronously.

Question

When should you use Transformations.switchMap over Transformations.map?

Answer

Use switchMap when each value in the source LiveData produces a different inner LiveData (e.g., database query by ID). Use map when applying a simple transformation to each value.

Question

Why is LiveData lifecycle-aware?

Answer

LiveData is tied to a LifecycleOwner. It only delivers updates to observers in STARTED or RESUMED state and auto-removes them at DESTROYED state.

Question

What is the modern alternative to LiveData in Android?

Answer

StateFlow in ViewModel with collectAsStateWithLifecycle() in Compose. StateFlow is more flexible, coroutines-native, and works on any thread.

Revision Notes

Key Takeaways

  • 1. LiveData automatically removes observers at DESTROYED state, preventing memory leaks
  • 2. setValue is main-thread only; postValue can be called from background threads
  • 3. Transformations.map applies a function lazily; switchMap switches to a new inner LiveData
  • 4. MediatorLiveData merges multiple LiveData sources into a single observable
  • 5. StateFlow with collectAsStateWithLifecycle is the modern replacement for LiveData

Interview Tips

  • Explain the lifecycle awareness of LiveData and how it prevents crashes
  • Know when to use setValue vs postValue with concrete examples
  • Discuss Transformations.switchMap for database queries dependent on parameters
  • Be ready to compare LiveData with StateFlow and explain the migration path

Cheat Sheet

LiveData Cheat Sheet

Creation:

  • MutableLiveData<T>() — mutable, private in ViewModel
  • val liveData: LiveData<T> — immutable, exposed to UI

Setting Values:

  • .value = x — main thread, synchronous (setValue)
  • .postValue(x) — any thread, asynchronous

Observation:

  • liveData.observe(lifecycleOwner) { value -> ... } — in Activity/Fragment
  • liveData.observeAsState() — in Compose
  • Observer auto-removed at DESTROYED

Transformations:

  • .map { x -> transform(x) } — transform each value
  • .switchMap { x -> innerLiveData } — switch to new LiveData
  • MediatorLiveData — merge multiple sources

Threading:

  • setValue: main thread only
  • postValue: any thread, posts to main
  • Transformations run on the same thread as the source

LiveData vs StateFlow:

  • LiveData: built-in lifecycle awareness, observeAsState()
  • StateFlow: native coroutines, more operators, preferred for new code