If you've been working with Android for a while, LiveData probably doesn't feel unfamiliar.
I've used MutableLiveData quite a lot in Android projects. It was simple, worked well with the Android lifecycle, and for a long time it was one of those APIs I didn't really think twice about using.
Then KMP came into the picture.
And that's when I started looking at LiveData a little differently.
Not because LiveData is bad. It isn't.
The problem is simply that LiveData is an Android API.
When we're writing shared code with Kotlin Multiplatform, one of the main goals is to keep that code independent from a specific platform as much as possible.
That's where Flow starts making a lot more sense.
For example, in shared code, I can have:
val users: Flow<List<User>>
There is nothing Android-specific about it.
And when I need to keep some state, I can use StateFlow:
private val _user = MutableStateFlow<User?>(null)
val user = _user.asStateFlow()
This feels much more natural in a KMP project because Flow is already part of the Kotlin Coroutines ecosystem.
But is Flow really better than LiveData?
I don't think that's the right question.
If you're building a normal Android application, LiveData can still be perfectly fine.
If your whole codebase is Android-specific, using an Android-specific API isn't really a problem.
The situation changes when the same code needs to work on Android and iOS.
For example, if I put this in commonMain:
private val _user = MutableLiveData<User?>()
I'm immediately bringing an Android dependency into shared code.
That's something I usually want to avoid.
With:
private val _user = MutableStateFlow<User?>(null)
I don't have that problem.
The shared code can stay shared.
And honestly, that's probably the biggest reason I'd choose Flow for KMP.
StateFlow feels familiar if you come from LiveData
When I first started using StateFlow, I found it easier to understand by comparing it with what I already knew.
With LiveData:
private val _username = MutableLiveData<String>()
val username: LiveData<String> = _username
With StateFlow:
private val _username = MutableStateFlow("")
val username: StateFlow<String> = _username
They're not the same thing, and there are differences in how they behave.
But conceptually, both can be used to expose some state to the UI.
The nice part with StateFlow is that it fits naturally with coroutines, which you're probably already using in a KMP project anyway.
What about SharedFlow?
This was another thing I had to get used to.
Not everything coming from a ViewModel is actually "state".
Sometimes you just want to tell the UI that something happened.
For example:
private val _events = MutableSharedFlow<UiEvent>()
val events = _events.asSharedFlow()
Maybe you want to show a snackbar.
Maybe navigation needs to happen.
Maybe you want to send some one-time UI event.
That's where SharedFlow can be useful.
The way I personally keep it in my head is pretty simple:
StateFlow→ I need the current stateSharedFlow→ I need to send eventsFlow→ I need a stream of values
There is obviously more to each of them, but this mental model is enough for me most of the time.
Does that mean LiveData should be removed from Android projects?
Definitely not.
If I opened an existing Android project and saw LiveData everywhere, I wouldn't immediately start replacing it with StateFlow.
If it works, it works.
There needs to be a reason for a migration.
For a new KMP project, though, I'd probably start with Flow-based APIs from the beginning.
It just saves me from introducing Android-specific things into code that is supposed to be shared later.
And I think that's an important distinction.
The question isn't really:
LiveData or Flow — which one is better?
For me, it's more like:
Where is this code going to live?
If it's Android-only, LiveData can make sense.
If it's shared KMP code, Flow is usually the more natural choice.
One thing I like about Flow in KMP
The biggest benefit isn't actually the API itself.
It's that the same concept can stay in the shared layer.
For example:
class UserViewModel {
private val _user = MutableStateFlow<User?>(null)
val user = _user.asStateFlow()
// shared logic
}
I can keep the logic in common Kotlin instead of creating one approach for Android and another one for iOS just because the observable type is different.
The UI on each platform can then consume that state in the way that makes sense for that platform.
That separation is one of the things I really like about KMP.
So what would I choose?
For an Android-only project, I wouldn't have a problem with LiveData.
For shared KMP code, I'd generally go with:
StateFlowfor state.SharedFlowfor events.- Regular
Flowwhen I just need a stream of values.
That's probably enough of a rule for me.
I'm not saying everyone should go through their existing Android codebase and replace every MutableLiveData with MutableStateFlow. There are better things to spend your time on.
But if you're starting to move from Android development into Kotlin Multiplatform, I think learning Flow properly is worth the effort. Once Flow, StateFlow, and SharedFlow start making sense, working with shared KMP code becomes a lot easier.