Something about migrating LiveData to Flow
We usually hear that Kotlin’s Coroutines’s Flow can replace the Android Jetpack Compose liveData. Is that true?
It’s. It’s pretty simple to use Flow to replace the LiveData.
For example, we have ViewModel called SampleViewModel, which has a LiveData
class SampleViewModel : ViewModel() {
val liveData: MutableLiveData<String> = MutableLiveData()
}
We usually use LiveData comes from ViewModel in Activity like this:
viewModel.liveData.observe(this) {
Log.i("SampleDemo", "liveData observe : $it")
}
Or in Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.liveData.observe(viewLifecycleOwner) {
Log.i("SampleDemo", "liveData observe : $it")
}
}
You want to replace it to become the Flow. I should use StateFlow. It should fit the LiveData trait.
class SampleViewModel : ViewModel() {
val stateFlow: MutableStateFlow<String> = MutableStateFlow("Test")
}
We usually use StateFlow comes from ViewModel in Activity like this:
lifecycleScope.launch {
viewModel.stateFlow.collect{
Log.i("SampleDemo", "state flow collect :$it")
}
}
Or we can make it simple by using syntactic sugar:
viewModel.stateFlow.onEach {
Log.i("SampleDemo", "state flow collect :$it")
}.launchIn(lifecycleScope)
It’s similar; use it in the Fragment. You need to use the correct coroutine scope:
viewModel.stateFlow.onEach {
Log.i("SampleDemo", "state flow collect :$it")
}.launchIn(viewLifecycleOwner.lifecycleScope)
In this case, the result will be assigned to LiveData and emitted by a flow a few seconds after the Activity is created.
This shows a difference between LiveData and StateFlow: LiveData doesn’t have a default value; StateFlow has a default value.
Then, if we give liveData a default value, is it the same as StateFlow?
val liveData: MutableLiveData<String> = MutableLiveData("Test")
None. If we use the lifecycle.currentState to observe the observer invoke timing:
viewModel.liveData.observe(this) {
Log.i("SampleDemo", "lifecycle.currentState : ${lifecycle.currentState}")
Log.i("SampleDemo", "liveData observe : $it")
}
viewModel.stateFlow.onEach {
Log.i("SampleDemo", "viewModel.stateFlow ${lifecycle.currentState}")
Log.i("SampleDemo", "state flow collect :$it")
}.launchIn(lifecycleScope)
Here is what it runs:
You can see the difference between StateFlow and LiveData. The Flow collects its default value at the first init, but the LiveData notifies the observer while the first value is assigned.
Another important thing that should be noticed is that asynchronous tasks, like API requests, are still running, and you let Activity enter into the background. Then, You receive the result while your Activity state is after onStop.
I do this at my viewModel
viewModelScope.launch {
delay(5000)
liveData.value = "Testing"
stateFlow.value = "Testing"
}
I delayed five seconds to leave enough time to press home to let the Activity enter the background.
Then we will get the message like this:
The flow collection can still be invoked even if the Activity’s state is onStop.
Then, you resume the Activity back to the foreground. The LiveData received the later result received.
Here is the most significant difference:
We see. The Flow collected almost immediately after its launch, but LiveData noticed its observer after onStart.
The Flow doesn’t seem like LiveData received the result while the Activity stopped and notified the observer after the Activity restarted. Flow won’t care about the Activity lifecycle in the background or foreground. It will keep collecting until the Activity is destroyed. This might cause some performance problems (Update the view state that is unnecessary. although it’s very tiny that the user is hard to notice.)
So, if you want to ensure the StateFlow collect timing is the same as the LiveData observer notice timing. You will need to use launchWhenStarted
:
lifecycleScope.launchWhenStarted {
viewModel.stateFlow.collect{
Log.i("SampleDemo", "flow current lifecycle state ${lifecycle.currentState}")
Log.i("SampleDemo", "flow launchWhenStarted $it")
}
}
So sad. Because it’s beautiful if we can use launchIn
, for me, I may write an extension for this like:
fun <T> Flow<T>.launchInWhenStarted(scope: LifecycleCoroutineScope): Job = scope.launchWhenStarted {
collect() // tail-call
}
So I can write it like launchIn
Format:
viewModel.stateFlow.onEach
Log.i("SampleDemo", "flow current lifecycle state ${lifecycle.currentState}")
Log.i("SampleDemo", "flow launchWhenStarted $it")
}.launchInWhenStarted(lifecycleScope)
And here is the other difference. If we emit and assign the value like this:
viewModelScope.launch {
stateFlow.value = "Test"
liveData.value = "Test"
}
Then we got the result like this:
You can see that Flow works just like LiveData.
But…
The launchWhenX
serial has been deprecated. You might notice it while you use it.
Why? Why? I keep asking about it. But I don’t get a satisfying answer. Accroding the documentation in the code.
It can lead to wasted resources in some cases. So, it has been deprecated. But What cases? LiveData keeps their observers while Activity is in the background, too. Isn’t it? Nice job, Google.
Well, After I did some research on this post. I realize why this method has been deprecated.
The Flow may produce the data in hot observable form. e.g.:
val simpleFlow = channelFlow {
while (true) {
Log.i("SampleDemo", "run in flow")
delay(1000) // represent some long time process
send("Test")
}
}.flowOn(Dispatchers.IO)
And we collect it by launchWhenStarted
this:
lifecycleScope.launchWhenStarted {
viewModel.simpleFlow.collect {
Log.i("SampleDemo", "flow current lifecycle state ${lifecycle.currentState}")
Log.i("SampleDemo", "flow launchWhenStarted $it")
}
}
Then there is the log:
We see. The Flow will keep processing even though the Activity has already stopped. It’s truly causing some potential wasted resources when you use the launchWhenStarted
.
So, That’s the reason why launchWhenX
serial has been deprecated. The document recommends replacing the launchWhenX
serial to repeatOnLifecycle
.
But this literally has more difference with LiveData observation.
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.stateFlow.collect {
Log.i("SampleDemo", "flow repeatOnLifecycle current lifecycle state ${lifecycle.currentState}")
Log.i("SampleDemo", "flow repeatOnLifecycle $it")
}
}
}
Or you may use syntactic sugar:
viewModel.stateFlow.flowWithLifecycle(lifecycle,Lifecycle.State.STARTED).onEach {
Log.i("SampleDemo", "flow repeatOnLifecycle current lifecycle state ${lifecycle.currentState}")
Log.i("SampleDemo", "flow repeatOnLifecycle $it")
}.launchIn(lifecycleScope)
Here is what it looks like:
You can see the Flow collected whenever the Activity restarts. Not just like the liveData notifies its observer only when the data is updated.
And if you use it at the wrong lifecycle step, like launching it at onStart.
override fun onStart() {
super.onStart()
Log.i("SampleDemo", "onStart")
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.stateFlow.collect {
Log.i("SampleDemo", "flow repeatOnLifecycle current lifecycle state ${lifecycle.currentState}")
Log.i("SampleDemo", "flow repeatOnLifecycle $it")
}
}
}
}
It will have a lot of collections at the same time.
But the IDE will warn you when you’re doing this.
Summary
The Flow can replace the LiveData. But If you want to replace the LiveData. You will need to notice different traits between LiveData and Flow. Those two have different development logic.
Those two things are similar but essential differences. LiveData represents the UI state. StateFlow can represent the UI state and more.
In my opinion. The Flow is suitable at Jetpack Compose. The LiveData is suitable for Android Views.
So, if you’re using LiveData a lot in the current project, I suggest you keep using it. You don’t need to change or migrate to flow without any reason.
But if you start a new project and want to try a new thing and different design and development logic. I will suggest using Stateflow and SharedFlow.