Let's say you're building an Android app with Jetpack Compose, and you keep running into this question:
"Should I use LiveData with observeAsState() or just stick to mutableStateOf?"
You're not alone—this is a super common confusion, especially if you're migrating old code or mixing Compose with XML.
Let's break it down in a no-nonsense, real-world way.
1. What Are These Things, Anyway?
LiveData
- Comes from Android's architecture components (used for years in XML-based apps)
- Knows about the Android lifecycle (activity/fragment)
- Designed for sharing state between ViewModel and UI, especially when config changes (like rotation) happen
mutableStateOf
- Part of Compose's own state system
- Just a simple observable property — when it changes, the UI recomposes
- Doesn't know (or care) about Android lifecycles
- Pure Kotlin, works even outside Android
2. Why Does Jetpack Compose Still Support LiveData?
Because almost every "legacy" ViewModel in big apps exposes LiveData!
Compose tries to be compatible, so you can use observeAsState() like this:
val user by viewModel.userLiveData.observeAsState()Now, whenever LiveData updates, Compose will automatically update your UI. You don't have to write a bunch of boilerplate observers.
3. When Should You Use LiveData (With observeAsState)?
Here are the situations where LiveData still makes sense:
- Your ViewModel already exposes LiveData:
Don't rewrite everything at once. Use
observeAsState()and move on. - Mixing Compose and XML: If some screens use Compose, some still use XML, LiveData is the common language.
- You're getting data from Room, WorkManager, or other AndroidX libraries: These often output LiveData by default. No need to convert them unnecessarily.
- Lifecycle-sensitive stuff: LiveData auto-pauses updates when your screen is backgrounded, so you avoid leaks and wasted work.
4. When Should You Use mutableStateOf?
These are the situations where Compose state shines:
- UI-only state: Stuff like text field values, checkboxes, toggles — things that don't need to survive navigation or be shared.
- Pure Compose modules: If you're doing Compose-only features (maybe in a library, or shared KMP code), LiveData doesn't work — stick to Compose's state system.
- You want to keep things simple: No need to wrap everything in LiveData if it's not crossing screen boundaries or dealing with lifecycle weirdness.
Example:
var text by remember { mutableStateOf("") }
TextField(value = text, onValueChange = { text = it })5. What Do I Do In My ViewModel?
- If you want the value to survive config changes (rotation, etc.), and you might use XML and Compose together, expose LiveData.
- If you only use Compose, it's okay to do this:
class MyViewModel : ViewModel() {
var counter by mutableStateOf(0)
private set
fun increment() { counter++ }
}In your composable:
val vm: MyViewModel = viewModel()
Text("Counter is ${vm.counter}")
Button(onClick = { vm.increment() }) { Text("Increment") }6. What About StateFlow?
You'll also hear about StateFlow (from Kotlin coroutines). Short answer:
- It's the modern replacement for LiveData.
- It works with Compose via
collectAsStateWithLifecycle()(just like LiveData does withobserveAsState()). - It's pure Kotlin, more flexible, and recommended for new code.
But if your ViewModel already uses LiveData, there's no emergency to migrate.
7. Quick Cheat Sheet
- Mixing Compose & XML, or using Android libraries?
→ Expose LiveData, use
observeAsState()in Compose. - UI-only state, Compose-only feature?
→ Use
mutableStateOf/remember. - Starting fresh with coroutines?
→ Prefer StateFlow, use
collectAsStateWithLifecycle().
8. What Can Go Wrong?
- Don't forget a default value when you do
observeAsState()(likeemptyList()or""), or you might get crashes on the first run! - Don't mix too many types — try to pick one "source of truth" for each bit of state.
TL;DR (if your friend is running off to a meeting):
- Use LiveData +
observeAsState()for compatibility with old code and lifecycle-aware data. - Use
mutableStateOffor lightweight, UI-only state that lives/dies with the composable. - For new, complex/reactive state, StateFlow is best — but LiveData is still fine for many apps.
That's it! If you're ever stuck, just ask: "Do I need this data outside Compose? Does it need to survive config changes or be observed by XML? If not, keep it simple and use Compose state!"