Table of Contents
Introduction
LiveData is a core Architecture Component that lets your app hold data in a lifecycle aware way. It is designed to work together with ViewModel and MVVM, but in this chapter you will focus on what LiveData is, how it behaves, and how to use it from Activities and Fragments without repeating ViewModel concepts that are covered elsewhere.
What LiveData Is
LiveData is a data holder class that can be observed. It keeps a value in memory and notifies registered observers when that value changes. What makes it special is that it is lifecycle aware. It only sends updates to observers that are in an active lifecycle state, such as STARTED or RESUMED.
At its simplest, you can think of LiveData as:
- A container that stores a value, for example a
Stringor a list. - A mechanism that lets UI components, such as Activities and Fragments, observe changes in that value.
- A lifecycle aware notifier, which stops sending updates to UI components when they are not visible and avoids many memory leaks.
LiveData is a generic type, declared as LiveData<T>. For example, LiveData<Int> holds an integer and LiveData<List<User>> holds a list of users.
Basic LiveData Types
There are two main forms you will use:
LiveData<T> is read only. Observers can get its value through observation, but cannot modify it directly.
MutableLiveData<T> is a subclass that allows modification. You can change the value using value or postValue.
Typically, MutableLiveData is used internally where data is produced, while only LiveData is exposed to observers. This separation helps you keep a clear boundary between who can change the data and who can only read it.
For example, you might have a private mutable field and a public read only field referring to the same underlying data.
Creating and Updating LiveData
To create LiveData, you normally instantiate MutableLiveData and specify the type you want to store. You can set an initial value or assign it later.
Here is a simple example with an integer counter:
class CounterHolder {
// Internal mutable LiveData
private val _counter = MutableLiveData<Int>()
// Public read-only LiveData
val counter: LiveData<Int> = _counter
init {
_counter.value = 0
}
fun increment() {
val current = _counter.value ?: 0
_counter.value = current + 1
}
}The key update methods are:
value can be set from the main thread. You usually use this in code that already runs on the UI thread.
postValue can be called from any thread, such as inside background work. LiveData will schedule the update on the main thread.
Always update LiveData from the correct thread. Use value on the main thread and postValue when you are in a background thread. Calling value from a background thread can lead to errors or unexpected behavior.
For example, if you load data in a background thread you would do:
fun loadDataInBackground() {
Thread {
// Simulate some work
val result = "Loaded"
// Safe from background thread
_status.postValue(result)
}.start()
}Observing LiveData in Activities
The main way to use LiveData from an Activity is to observe it with a lifecycle owner. Activities implement LifecycleOwner, which lets LiveData know when they are started, resumed, stopped, and destroyed.
The typical pattern is:
- Obtain a reference to the
LiveData. - Call
observe, passingthisas the lifecycle owner and a lambda that receives the updated value. - Update your UI inside the observer.
Here is a minimal example that displays a counter in a TextView:
class MainActivity : AppCompatActivity() {
private lateinit var counterHolder: CounterHolder
private lateinit var textViewCounter: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textViewCounter = findViewById(R.id.textCounter)
counterHolder = CounterHolder()
counterHolder.counter.observe(this) { value ->
textViewCounter.text = value.toString()
}
}
}
The observe call registers the Activity as an observer. LiveData will then:
Send the current value immediately if one exists and the Activity is at least in the STARTED state.
Send any later changes as long as the Activity stays STARTED or RESUMED.
Stop sending updates when the Activity is STOPPED and remove the observer when the Activity is destroyed.
You do not need to manually unsubscribe. The Activity lifecycle owner takes care of it.
Observing LiveData in Fragments
Fragments are also LifecycleOwners. Observing LiveData inside a Fragment is similar to observing it in an Activity, but you usually use the Fragment’s view lifecycle for UI elements.
You observe using viewLifecycleOwner instead of this. This is important because the Fragment view can be destroyed and recreated while the Fragment itself stays in memory.
Example:
class CounterFragment : Fragment(R.layout.fragment_counter) {
private lateinit var counterHolder: CounterHolder
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
counterHolder = CounterHolder()
val textViewCounter: TextView = view.findViewById(R.id.textCounter)
counterHolder.counter.observe(viewLifecycleOwner) { value ->
textViewCounter.text = value.toString()
}
}
}
If you use the Fragment instance as the lifecycle owner instead of viewLifecycleOwner, the observer would remain active even after the Fragment’s view is destroyed. That can cause updates to reference old views or cause crashes due to trying to access views that no longer exist.
In Fragments, always observe LiveData with viewLifecycleOwner when updating views. This keeps observers in sync with the view lifecycle and avoids crashes from accessing destroyed views.
LiveData and Lifecycle Awareness
The lifecycle awareness of LiveData is central to how it behaves. Each LifecycleOwner goes through states such as INITIALIZED, CREATED, STARTED, RESUMED, STOPPED, and DESTROYED.
LiveData considers an observer active when its owner is at least in the STARTED state. An inactive observer keeps the registration but does not receive updates.
The key behaviors are:
When you call observe, the observer is immediately added. If the lifecycle is already at least STARTED, LiveData immediately sends the current value if any.
If the lifecycle moves to STOPPED, LiveData does not call the observer anymore until the lifecycle moves back to STARTED.
If the lifecycle reaches DESTROYED, LiveData automatically removes the observer.
This behavior gives you several advantages. You avoid many memory leaks because LiveData does not hold references to destroyed UI components. You avoid unnecessary work while the UI is not visible because updates are not delivered to stopped components. You also avoid manual subscription and unsubscription code in onStart and onStop.
Handling Null Values
LiveData can hold nullable types. For example, you might declare LiveData<String?> or simply use MutableLiveData<String>() without an initial value, which means its value is null until you set it.
Inside your observer you must handle the possibility that the value is null. Otherwise you risk a crash if you assume it is non null.
Example with safe handling:
counterHolder.status.observe(this) { status ->
val text = status ?: "Unknown"
textViewStatus.text = text
}
If you prefer to avoid nulls in observers you can provide a non null default when you update the LiveData, or you can transform the LiveData into another one that provides default values before you observe it. Transformations are a separate topic, but the main point is to be aware that value may be null if you did not initialize it.
Never assume LiveData has a non null value unless you have initialized it explicitly. Always handle null in observers using safe calls or default values.
One Time Events vs Continuous Data
LiveData works naturally for continuous data, such as a list of items or a current score, where you want observers to be updated with the latest value whenever they become active.
However, one time events like navigation commands or showing a message are different. If you store such events directly in LiveData, new observers or reattached observers can receive an old event again. For example, an Activity recreation after a rotation can cause a toast message to show twice.
To manage one time events you usually wrap the content in a special wrapper or use a different mechanism. The full pattern is beyond this chapter, but you should remember that LiveData replays the current value to any new active observer. This is a benefit for state data but can be a problem for single events.
Using LiveData with Background Work
Although LiveData is designed for the UI layer, it often receives updates from background work. You might load data from a network or a database, then update the LiveData with the result.
You typically follow this pattern:
Prepare LiveData in the holder of your data.
Start background work using threads or coroutines.
When the work completes, call postValue on MutableLiveData.
The UI observes the LiveData and updates automatically. You do not need to manually interact with Activities or Fragments from your background code.
For example:
class UserLoader {
private val _userName = MutableLiveData<String>()
val userName: LiveData<String> = _userName
fun loadUser() {
Thread {
// Simulate long running task
Thread.sleep(1000)
_userName.postValue("Alice")
}.start()
}
}
In your Activity, when you call loadUser, the background thread runs, then the LiveData is updated, and your observer in the Activity receives the new name and updates the UI.
Combining LiveData Sources
Sometimes you need data that depends on more than one LiveData source. For instance, you might want to display a formatted string that depends on two separate LiveData values.
There are utilities such as MediatorLiveData and transformation helpers that let you watch multiple sources and produce a new value whenever any of them change. The detailed patterns for combining LiveData will appear in more advanced architecture discussions, but you should understand that LiveData is not limited to simple one field usage.
As a simple illustration, MediatorLiveData lets you add multiple sources and react when any one of them changes, then you can calculate and set a new value inside the mediator.
Testing LiveData
Since LiveData is a component that holds data and not just UI, you can test it. To test LiveData, you must either make it execute immediately or observe it in a controlled environment.
A common test approach is to observe the LiveData synchronously and collect emitted values. Android provides helpers to make LiveData work in unit tests without a full Activity or Fragment. The main idea is that you treat LiveData as a source of values and verify those values after performing some actions that should trigger changes.
The exact test tools and patterns are part of the testing chapter, but it is useful to know that LiveData can be tested in isolation, and you are not restricted to manual testing in the running app.
When to Use LiveData
LiveData is primarily for:
Data that the UI displays and that can change while the screen is visible.
States that belong to a screen or feature, for example loading indicators, lists of items, error messages that can be derived from data, and selections.
Values that should survive configuration changes when tied to a ViewModel, which is covered elsewhere.
LiveData is less appropriate for one time events, very short lived values that do not need to survive Activity recreation, or deep background processes that have no direct connection to the current UI. In those cases, you might lean on other mechanisms or patterns.
By using LiveData correctly, you let your UI react automatically to data changes, stay in sync with lifecycles, and avoid a lot of manual bookkeeping.