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?
2. When should you use postValue instead of setValue?
3. What does Transformations.switchMap do when the source LiveData emits a new value?
4. Why is LiveData exposed as LiveData<T> instead of MutableLiveData<T>?
Flashcards
Question
What is the difference between setValue and postValue in LiveData?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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 ViewModelval 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/FragmentliveData.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