Question
Fixing observeAsState Delegation Errors in Jetpack Compose Kotlin
Question
In Jetpack Compose, I am trying to read a LiveData value using observeAsState(), but I get this error:
Type 'State<List<User>?>' has no method 'getValue(Nothing?, KProperty<*>)' and thus it cannot serve as a delegate
Here is my code:
@Composable
fun UserScreen(userViewModel: UserViewModel) {
val items: List<User> by userViewModel.fetchUserList.observeAsState()
UserList(userList = items)
}
And my ViewModel:
class UserViewModel : ViewModel() {
private val dataSource = UserDataSource()
val fetchUserList = liveData {
emit(dataSource.dummyUserList)
}
}
Why does this happen, and what is the correct way to use observeAsState() with LiveData in Jetpack Compose?
Short Answer
By the end of this page, you will understand how observeAsState() converts LiveData into Compose State, why the by syntax sometimes fails, how nullability affects the result, and how to fix the delegate error in a clean and idiomatic way.
Concept
In Jetpack Compose, observeAsState() is used to bridge older Android LiveData into the Compose state system.
When you call:
val itemsState = userViewModel.fetchUserList.observeAsState()
Compose gives you a State<T?> object. That means:
- it is a wrapper around a value
- the value is read through
.value - the value may be
nulluntilLiveDataemits something
The by keyword is Kotlin property delegation syntax. It lets you write this:
val items by userViewModel.fetchUserList.observeAsState()
instead of this:
val items = userViewModel.fetchUserList.observeAsState().value
However, delegation only works if the required operator function is available. In Compose, this usually comes from the import:
import androidx.compose.runtime.getValue
Without that import, Kotlin does not know how to delegate a State<T> object to a local property, so it reports the getValue(...) error.
There is also a second issue in your example: observeAsState() without an initial value returns a nullable state, so the type is effectively State<List<User>?>. That means the delegated value is also nullable:
val items: List<User>? by ...
If you declare items as non-null List<User>, the types do not match unless you provide an initial value.
Why this matters in real programming:
- Compose UI updates automatically when state changes
LiveDatais often nullable at first- UI code must handle loading or empty states safely
- understanding delegation and state wrappers helps avoid confusing compiler errors
Mental Model
Think of LiveData as a mailbox that receives updates over time.
observeAsState() turns that mailbox into a Compose-readable container.
- The mailbox is
LiveData - The container is
State<T> - The actual letter inside is
value
Using .value is like opening the container directly.
Using by is like asking Kotlin to automatically open the container for you.
But for Kotlin to do that automatically, it needs the correct delegation tool imported. If that tool is missing, Kotlin says the container cannot serve as a delegate.
Syntax and Examples
The core syntax looks like this:
val usersState = viewModel.fetchUserList.observeAsState()
val users = usersState.value
Or with delegation:
import androidx.compose.runtime.getValue
val users by viewModel.fetchUserList.observeAsState()
Example 1: Using .value
@Composable
fun UserScreen(userViewModel: UserViewModel) {
val itemsState = userViewModel.fetchUserList.observeAsState()
val items = itemsState.value ?: emptyList()
UserList(userList = items)
}
This works because:
observeAsState()returnsState<List<User>?>itemsState.valuemay benull?: emptyList()provides a safe fallback
Example 2: Using by with an initial value
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
@Composable
fun UserScreen(userViewModel: UserViewModel) {
val items by userViewModel.fetchUserList.observeAsState(initial = emptyList())
UserList(userList = items)
}
This is often the cleanest approach.
Why it works:
initial = emptyList()makes the state non-null from the UI's perspectivebyreads the state value directlygetValueimport enables delegation
Example 3: Explicit nullable handling
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
@Composable
fun UserScreen(userViewModel: UserViewModel) {
val items: List<User>? by userViewModel.fetchUserList.observeAsState()
if (items == null) {
LoadingView()
} else {
UserList(userList = items)
}
}
Use this style when null has meaning, such as "still loading."
Step by Step Execution
Consider this version:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
@Composable
fun UserScreen(userViewModel: UserViewModel) {
val items by userViewModel.fetchUserList.observeAsState(initial = emptyList())
UserList(userList = items)
}
Here is what happens step by step:
UserScreen(...)is called by Compose.userViewModel.fetchUserListis aLiveData<List<User>>.observeAsState(initial = emptyList())starts observing thatLiveData.- Before
LiveDataemits real data, Compose usesemptyList()as the current value. - Because
getValueis imported,bycan unwrap theStateautomatically. itemsis now a regularList<User>value, not aState<List<User>>.UserList(userList = items)receives an empty list first.- Later,
LiveDataemitsdummyUserList. - Compose sees the state change and recomposes
UserScreen. itemsnow contains the emitted list, andUserListupdates automatically.
This is the key Compose pattern: state changes trigger recomposition.
Real World Use Cases
This pattern is used whenever UI reads changing data from a ViewModel.
Common scenarios
- User lists: display users from a repository or database
- API responses: show products, posts, or comments after a network request
- Form state: update the screen when validation messages change
- Settings screens: observe saved preferences and reflect them in UI
- Loading screens: show progress until data arrives
Example: Product list from LiveData
@Composable
fun ProductScreen(viewModel: ProductViewModel) {
val products by viewModel.products.observeAsState(initial = emptyList())
ProductList(products)
}
Example: Nullable state for loading
@Composable
fun ProfileScreen(viewModel: ProfileViewModel) {
val profile by viewModel.profile.observeAsState()
when {
profile == null -> LoadingView()
else -> ProfileCard(profile)
}
}
In real apps, choosing between nullable state and an initial value depends on whether you want to represent loading explicitly.
Real Codebase Usage
In real projects, developers usually use one of these patterns.
1. Provide an initial value for collections
For lists, maps, and sets, an initial empty collection is common.
val users by viewModel.fetchUserList.observeAsState(initial = emptyList())
This avoids repeated null checks in UI code.
2. Use nullable state to represent loading
val user by viewModel.user.observeAsState()
if (user == null) {
LoadingView()
} else {
UserDetails(user)
}
This is useful when null means "not loaded yet."
3. Use guard clauses for clean UI logic
val users by viewModel.fetchUserList.observeAsState()
if (users == null) {
LoadingView()
return
}
UserList(userList = users)
This keeps the main rendering path simple.
4. Prefer UI-friendly state in the ViewModel
In larger codebases, developers often expose state already shaped for the UI, such as:
- empty list instead of null
- sealed UI state like Loading / Success / Error
- domain-specific state objects
5. Migrate toward StateFlow in newer code
Many modern Compose codebases prefer StateFlow with collectAsState(), but LiveData with observeAsState() is still valid and common in existing apps.
Common Mistakes
1. Forgetting the getValue import
Broken code:
val items by viewModel.fetchUserList.observeAsState()
If you use by, you usually need:
import androidx.compose.runtime.getValue
Fix:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
2. Treating nullable state as non-null
Broken code:
val items: List<User> by viewModel.fetchUserList.observeAsState()
Why it is wrong:
observeAsState()withoutinitialreturns a nullable valueitemsshould beList<User>?, or you should provide an initial value
Fix:
val items by viewModel.fetchUserList.observeAsState(initial = emptyList())
or
val items: List<User>? by viewModel.fetchUserList.observeAsState()
3. Forgetting the observeAsState import
Broken code may fail or not resolve correctly if this import is missing:
import androidx.compose.runtime.livedata.observeAsState
4. Passing nullable data into a non-null parameter
Broken code:
val items: List<User>? by viewModel.fetchUserList.observeAsState()
UserList(userList = items)
If UserList expects List<User>, this fails.
Fix:
UserList(userList = items ?: emptyList())
5. Mixing up State<T> and T
Broken code:
val items = viewModel.fetchUserList.observeAsState()
UserList(userList = items)
Why it is wrong:
itemsis aState<List<User>?>UserListlikely expectsList<User>
Fix:
val items = viewModel.fetchUserList.observeAsState().value ?: emptyList()
UserList(userList = items)
Comparisons
| Concept | What it returns | How you read the value | Typical use |
|---|---|---|---|
observeAsState() | State<T?> or State<T> with initial value | .value or by | Read LiveData in Compose |
mutableStateOf() | MutableState<T> | .value or by | Local Compose state |
collectAsState() | State<T> | .value or by | Read Flow or StateFlow in Compose |
by vs .value
| Style | Example | When to use |
|---|---|---|
by | val items by state | Cleaner syntax when imports are set up |
.value | val items = state.value | Clear and explicit, useful for beginners |
Nullable vs initial value
| Approach | Example | Benefit | Tradeoff |
|---|---|---|---|
| Nullable state | observeAsState() | Can represent loading naturally | Requires null handling |
| Initial value | observeAsState(initial = emptyList()) | Simpler UI code | May hide loading state |
LiveData vs StateFlow
| Feature | LiveData | StateFlow |
|---|---|---|
| Common in older Android apps | Yes | Less common in older apps |
| Compose integration | observeAsState() | collectAsState() |
| Modern Kotlin-first choice | Sometimes | Usually yes |
| Lifecycle awareness built in | Yes | Usually handled with lifecycle-aware collection |
If your project already uses LiveData, observeAsState() is the correct bridge.
Cheat Sheet
// Use LiveData in Compose
val state = viewModel.data.observeAsState()
// Read value explicitly
val value = state.value
// Or use delegation
import androidx.compose.runtime.getValue
val value by viewModel.data.observeAsState()
Key rules
observeAsState()convertsLiveData<T>into ComposeState<T?>- Without an initial value, the result is usually nullable
- Use
initial = ...when you want a non-null value immediately - If you use
by, import:androidx.compose.runtime.getValueandroidx.compose.runtime.livedata.observeAsState
Safe patterns
val items by viewModel.fetchUserList.observeAsState(initial = emptyList())
val items = viewModel.fetchUserList.observeAsState().value ?: emptyList()
val items: List<User>? by viewModel.fetchUserList.observeAsState()
Common fix for this exact error
import androidx.compose.runtime.getValue
Another common fix
Provide an initial value if your UI expects non-null data:
val items by viewModel.fetchUserList.observeAsState(initial = emptyList())
Edge case
If null is meaningful, do not replace it with an empty list automatically. Use explicit loading or error UI instead.
FAQ
Why does observeAsState() return a nullable value?
Because the LiveData may not have emitted a value yet when the composable first runs.
How do I fix the getValue delegate error in Compose?
Import:
import androidx.compose.runtime.getValue
and make sure you also import observeAsState from the LiveData Compose package.
Should I use .value or by with Compose state?
Both are correct. .value is more explicit. by is shorter and more idiomatic once you understand delegation.
What initial value should I use for a list?
Usually emptyList(), unless your UI needs to distinguish between "loading" and "loaded but empty."
Can I pass LiveData<List<User>> directly to a composable?
You usually should not. Convert it to Compose state inside the composable with observeAsState() and pass plain UI data downward.
Is LiveData still okay in Jetpack Compose?
Yes. It works well with observeAsState(). Newer projects often prefer StateFlow, but LiveData is still valid.
Why does my composable re-run when data changes?
Because Compose tracks state reads. When the observed state changes, Compose recomposes affected UI.
Mini Project
Description
Build a small Compose screen that displays a list of users from LiveData. The project demonstrates how to observe LiveData safely, provide an initial value, and render the result without null-related crashes or delegation errors.
Goal
Create a composable that reads a user list from a ViewModel using observeAsState() and displays it as a simple text list.
Requirements
- Create a
ViewModelthat exposes aLiveData<List<User>>. - In a composable, observe that
LiveDatausingobserveAsState(). - Avoid nullable UI crashes by providing an initial value or handling null safely.
- Display each user's name in the UI.
- Use valid Compose state-reading syntax.
Keep learning
Related questions
Accessing Kotlin Extension Functions from Java
Learn how Kotlin extension functions are compiled and how to call them correctly from Java with clear examples and common pitfalls.
Add a Maven Repository URL in Gradle Kotlin DSL (build.gradle.kts)
Learn how to add Maven Central and a custom Maven repository URL in Gradle Kotlin DSL using build.gradle.kts syntax.
Allow HTTP and HTTPS in Android 9 Pie with Network Security Configuration
Learn how Android 9 Pie handles cleartext HTTP traffic and how to allow HTTP and HTTPS safely using network security config.