LiveData Was a Breakthrough, but It's Now the Wrong Abstraction for Most of Your App
The first time I encountered LiveData, I thought it was amazing.
Back then, Android development was full of Callbacks, lifecycle issues, and memory leak risks.
The emergence of LiveData gave Android development its first data observation solution integrated with the lifecycle.
At that time, I thought:
LiveData is really great, simple and easy to use.
But a few years later, I use LiveData less and less.
In new projects now, I basically no longer actively choose LiveData, and instead use Kotlin Flow more.
This is because as the complexity of Android applications increases, some of LiveData's own design limitations become more and more apparent.
1. Why did LiveData impress me when it first appeared?
What was Android asynchronous development like before LiveData?
Lots of Callbacks:
api.getUser(new Callback<User>() {
@Override
public void onSuccess(User user) {
textView.setText(user.name);
}
});
It looks simple, but hides many problems.
1. Lifecycle Issues
For example:
Open page
↓
Request network
↓
User goes back
↓
Activity destroyed
↓
API returns
↓
Update UI
↓
Crash
Developers needed to judge themselves:
if(!activity.isDestroyed()){
}
Lots of lifecycle judgment code.
2. Data and UI tightly coupled
Before:
Request success
↓
Callback
↓
Modify UI
Business code and UI updates mixed together.
LiveData appeared:
viewModel.user.observe(this){
name.text = it.name
}
Suddenly it became very natural.
It solved:
- Automatic lifecycle management
- Automatic unsubscription
- Automatic UI notification on data changes
At the time, this was a huge step forward.
2. LiveData's greatest value: giving Android lifecycle-aware data observation
LiveData's biggest design:
observe(owner){
}
It knows:
STARTED
RESUMED
DESTROYED
states.
For example:
Activity:
onStart
↓
Start receiving data
onDestroy
↓
Auto unbind
Developers no longer need to manually handle:
- Canceling listeners
- Preventing memory leaks
- Judging page state
So LiveData's core contribution:
It solved the problem of "data observation" and "lifecycle management" in the Android world.
3. But LiveData's first limitation: it is not a true data stream
This is one of LiveData's biggest problems.
LiveData:
val user = MutableLiveData<User>()
It is more like:
An observable data container.
It holds:
Current value
Then notifies:
Observers
But many scenarios in modern applications are essentially:
Data streams.
For example, search:
User input:
a
↓
ab
↓
abc
We want:
No input for 300ms
↓
Request abc
Flow:
queryFlow
.debounce(300)
.flatMapLatest {
search(it)
}
Very natural.
Because Flow expresses:
Data generation
↓
Data transformation
↓
Data filtering
↓
Data consumption
While LiveData focuses more on:
What is the current value
4. LiveData lacks complete data stream processing capabilities
Modern applications heavily need:
- map
- filter
- combine
- debounce
- retry
- catch
- flatMapLatest
Flow:
userFlow
.filter {
}
.map {
}
.combine(otherFlow){
}
These are all core capabilities of Flow.
Although LiveData has:
Transformations.map()
Transformations.switchMap()
Its capabilities are limited.
For something more complex:
For example:
User info
+
Configuration
+
Network status
+
Permission status
LiveData usually:
MediatorLiveData
Manually managing multiple data sources.
The code easily becomes more and more complex.
5. LiveData's second limitation: it is not suitable as a data type for the Data layer
This is a problem many projects encounter later on.
Early in many projects:
class UserRepository {
fun getUser():LiveData<User>
}
At the time, it felt:
Very convenient.
But the problem arises:
LiveData belongs to:
androidx.lifecycle.LiveData
It is essentially an Android lifecycle component.
Why shouldn't the Data layer expose LiveData?
Because the data layer focuses on:
How data is generated
How data changes
Not:
Is the Activity displayed
Is the Fragment destroyed
But LiveData inherently focuses on:
LifecycleOwner
For example:
STARTED
RESUMED
DESTROYED
This means:
The data layer starts to know about:
The Android lifecycle model.
For example:
Database:
@Query(
"select * from user"
)
fun observeUser():LiveData<User>
Looks great.
But what if:
A background Service needs user data:
Or:
A Worker needs to listen for data:
What do they do?
Because they don't have:
LifecycleOwner
So various conversions start appearing:
asFlow()
In fact:
If the data source was Flow from the start:
These problems wouldn't exist.
6. LiveData restricts how data can be consumed
Suppose:
Repository:
fun message():LiveData<Message>
UI:
No problem:
observe()
But:
Background task:
Coroutine
Would prefer:
collect()
But LiveData:
Its design goal is:
Observer.
Thus:
The data producer determines the consumer pattern.
Whereas with Flow:
fun message():Flow<Message>
The consumer decides for themselves:
UI:
collectAsState()
Background:
collect()
Testing:
first()
Same data source:
Different consumption methods.
7. LiveData has always been awkward for event handling
Classic problem:
One-time events.
For example:
Login success:
loginSuccess.value = true
Page:
observe {
Toast.makeText(
"Login successful"
)
}
Rotate the screen.
LiveData will re-emit:
true
Result:
The Toast appears again.
Thus emerged:
SingleLiveEvent
Or:
EventWrapper
But this also illustrates:
LiveData is more suitable for:
State.
Not suitable for:
Event streams.
Events:
Click once
Emit once
Consume once
State:
What is the current value
The semantics of the two are different.
8. LiveData's threading model is relatively simple, also limiting its capabilities
LiveData:
setValue()
Main thread.
Background:
postValue()
Looks simple.
But:
It hides some details.
For example:
postValue(1)
postValue(2)
postValue(3)
Ultimately:
You might only receive:
3
Reason:
LiveData's goal:
Preserve the latest state.
It is not a message queue.
But many scenarios require:
Event 1
Event 2
Event 3
All processed
For example:
- Download progress
- Message notifications
- Operation events
LiveData is not suitable.
9. LiveData combined with Kotlin Coroutines is not as natural as Flow
Now the Android mainstream:
Coroutine
+
Flow
But LiveData:
Usually:
viewModelScope.launch {
val result = repository.getUser()
liveData.value = result
}
In the middle:
Coroutine
↓
LiveData
↓
Observer
An extra layer.
Flow:
viewModelScope.launch {
repository.user()
.collect {
}
}
Naturally connected.
10. LiveData's biggest problem: great for simplicity, easy to lose control with complexity
LiveData:
Simple page:
val loading =
MutableLiveData<Boolean>()
Very comfortable.
But complex page:
Multiple LiveData:
UserLiveData
+
ConfigLiveData
+
NetworkLiveData
+
PermissionLiveData
Finally:
MediatorLiveData
Transformations
Event wrappers
Various utility classes
Increasingly like:
Implementing your own data stream framework.
11. What is the essential difference between LiveData and Flow?
LiveData:
A lifecycle-aware data container.
Flow:
A data stream that can be composed, transformed, and cancelled.
LiveData mindset:
Data changed
Notify me
Flow mindset:
Data itself is a stream
I want to process this stream
Summary
LiveData is not wrong.
It was a very important part of the Android architecture evolution.
It solved:
Lifecycle-safe data observation.
But it also has clear limitations:
It is not a complete data stream model
Limited data combination capabilities
Not suitable as a data type for the Data layer
Integration with coroutines is not natural enough
Inherent flaws in event handling
So:
LiveData is suitable for the UI layer, but should not become the data transmission model for the entire application.