It is not new that in Android app development tools are constantly being created and deprecated and we need to be up to date with that information. Kotlin Flow tool was released some years ago, and it was created to solve some behaviors that Android LiveData has. Nowadays, both tools are available to be implemented in our projects, but can they live together? Is one better compared to the other? Let’s have a look and analyze in which situations they are useful, their advantages and disadvantages.
LiveData
Taking a look into its documentation, LiveData is defined as “an observable data holder class”. While implementing a MVVM pattern in our application, some value can be set in a LiveData inside the ViewModel and the UI can observe any change and execute some code as a response. Here is an example of how to read and write a boolean through a LiveData:
Due to the fact that it was created by Android, LiveData is lifecycle-aware and it only works in the Main thread. This means that we need to use Dispatchers to make a responsible use of device resources. For example, if our app fetches some information from an API and then shows it to the user, IO dispatchers should be used for network connections and then, Main thread should be called to display that information. In the following example, we are creating a viewModelScope which lives inside the Main thread. Then, it calls the IO dispatcher to fetch users' information and then returns to the Main thread to set the LiveData:
Note: LiveData provides a postValue(T value) method, which, internally, sets the values without the necessity of handling Dispatchers.
Besides that, one characteristic that LiveData has is, when there is any configuration change or screen rotation, it will execute the last saved value on it. This can be a headache when trying to display “one shot events'', such as toasts or snackbars (imagine how annoying it can be to see an error dialog on every screen rotation).
That’s why LiveData is suitable to represent states: If there is a weather app which includes SUNNY, CLOUDY, RAINY and SNOWY states, when it is raining, we can show a water falling animation. If any configuration change happens, the same state will be sent and therefore, no changes on the UI will be shown (the same animation will be running).
In addition to this, LiveData lacks backpressure. If you haven’t listened to this term before, backpressure means that the consumer is not reading as fast as the producer sends information. Thus, if we are trying that the UI observes a fast data stream, the app will suffer unexpected behaviors and probably some data will be lost.
SingleLiveEvent
As a solution for handling One-Shot events, Android presented the SingleLiveEvent class. This was created as a subclass of MutableLiveData (it was aware of lifecycle) but it had a single Observer. The purpose of this change was to react to single events such as click, toasts, navigation, etc. Unfortunately, this helper was deprecated in 2021.
Flow
On the other hand, in 2019, Kotlin created Flow, which was defined as “An asynchronous data stream that sequentially emits values and completes normally or with an exception”. As its description says, Flow solves LiveData’s backpressure issue and also provides Intermediate operators such as map, filter, take or zip which helps to operate over the upstream to create another one (the downstream).
Additionally, Flow allows a better integration with Coroutines (both are Kotlin tools). Flow doesn’t need to use the Main thread to publish data in the UI, so it is easier to handle the Dispatchers and provide filtered data with less effort.
But it is not all bed of roses in Kotlin’s Flow. Due to the fact that it is not an Android tool, Flow is not lifecycle aware, thus, we need to pay attention when information is consumed and when not, so we destroy the channel that was created. It is said that Flow is a “cold stream” because it triggers the same code every time it is being collected (Cold Flows can be converted to Hot flows doing a modification in the shareIn property, but we are not going to talk about this on this article).
What is more, when there is any configuration change, instead of returning the last value on it, it will deliver again the first stream of data (if that’s the case). For this reason, we can conclude that Flow is a good choice to use when dealing with One-Shot events and data streams, but it is recommendable to use another approach when handling states (please check StateFlow).
When the screen rotates at 10:53:03, you can observe that Flow resets itself and the count starts from zero again.
SharedFlow
As a difference from Flow, SharedFlow is a hot type which means that independently from the number of collectors (none can be an option), it will continue emitting new values. When a new subscriber is related to a SharedFlow, it will get the remaining values from the cache and then the new ones.
Furthermore, SharedFlow inherits all the properties of Flow: has a nice integration with Kotlin Coroutines, we can use intermediate operators to filter data streams, and it is not lifecycle-aware. Here we have to pay special attention when enabling or disabling them, we are dealing with a Hot flow! Look how repeatOnLifecycle is used in the following code. Documentation says that “the given block is executed in a new coroutine when this Lifecycle is at least at state and suspends the execution until this Lifecycle is Lifecycle.State.DESTROYED.” Thus, we are handling carefully when it is being created and when it is being destroyed.
Regarding screen rotation and configuration changes, every time one of them happens, SharedFlow won’t emit a single value again. It solves one of the main problems that LiveData was presenting! Therefore, making it appropriate for One-shot events. Remember that SharedFlow can have multiple subscribers, so if we have an application that needs to run an alarm after a certain time, all collectors will receive the ending signal and the ones alive will execute the code associated.
StateFlow
StateFlow was built as an extension of SharedFlow, so both are hot flows, but the first one was especially designed to deal with states. If one state is set more than once consecutively, StateFlow won’t emit the value again, so the collector doesn’t have to execute the same code again. For the same reason, it is not appropriate to use it for one shot events: if you need to show an error toast more than once, it will be displayed only once.
Moreover, when a configuration change occurs, its value won’t change but it will be triggered again. Be careful with what the collector is doing, code will execute again if it is not handled carefully! Finally, a minor characteristic of this Flow type is that it will require an initial value. As a suggestion, try to think of every screen with this idea in mind, in some cases, you will need to create an extra initial case and then move it to what the business logic requires (null value can be a possibility too).
Conclusion
There is not a unique option between Android LiveData and Kotlin Flow to solve all development problems. If we need to choose a solution for our project, I will suggest going for SharedFlow and StateFlow. Both together can handle the basic scenarios that an application has to overcome (states and one-shot events) and both were created by the same company (check pros above). Of course, dealing with lifecycle can be a disadvantage, but after a bit of experience, you will be able to understand and solve it easily.