One of the most important parts of building efficient, performant and modern mobile applications is handling events. So many things happen in our apps, from user interaction, to UI updates, to pulling and displaying data from external sources. Over the years, many different tools and frameworks have been introduced to better help mobile developers build faster, better performing event driven apps.
Android LiveData
If you’ve worked with Android apps before, you’re probably already familiar with LiveData[Link]. It was introduced as part of the Android Architecture Components library.
The concept is fairly straightforward. A LiveData object is a lifecycle-aware data holder class that can stream or share data to attached observers. So if you were creating an app that needed to know when the user was finished entering text into a field, or when an API call was complete, LiveData would be very useful in this case.
Here’s a very simple example of what a LiveData use case might look like:
Let’s say you were creating an app that displayed movie info. There are 2 main screens. 1 screen that displays a list of movies, and another screen that displays information about each movie. The screen that displays the list of movies might be backed by a ViewModel, assuming the archictecture is MVVM.
class MoviesListViewModel : ViewModel() {
val moviesList: MutableLiveData<List<Movie>> by lazy {
MutableLiveData<List<Movie>>()
}
// Rest of the ViewModel...
}
Because a LiveData object is lifecycle-aware, it can be bound to both Activities and Fragments. You might have an Activity that sets up and observes your LiveData object for changes like this:
class MovieActivity : AppCompatActivity() {
private val viewModel: MoviesListViewModel by viewModels()
private val mAdapter: MoviesListAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Create the observer which updates the UI.
val moviesListObserver = Observer<List<Movie>> { movies ->
// Update the UI, in this case, show a list of movies returned from the API
mAdapter.setData(movies)
}
// Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
viewModel.moviesList.observe(this, moviesListObserver)
}
}
Whenever moviesList is updated in your ViewModel, any observers attached will be updated with its latest value.
What Are Some Benefits of LiveData?
- Always updated UI state
Because LiveData objects are lifecycle aware, they are bound to the attached Fragments and Activities that observe them. Whenever an observer becomes active, LiveData objects propogate the latest value to these observers.
- Avoid Memory Leaks
One of the issues early Android developers ran into involved the use of AsyncTasks[Link] to handle background work. A common problem with this was that sometimes the Activity that the task was started in was destroyed, while the task was still running, which prevented proper garbage collection.
- Properly Handle Configuration Changes
Device rotation and configuration changes can be handled seamlessly, since once the Activity/Fragment is recreated, its corresponding LiveData objects will become active and resend the latest values.
When Might LiveData Be Unneccesasry?
No tool or API is perfect. So when is LiveData not the best choice?
- Lifecycle Awareness Might Be Unneccessary
If you’re building an app where some work needs to be done off the main thread, and you don’t particularly need to update any UI state after it’s complete, then LiveData may not be the best fit. It might not be worth the extra work of setting up observers and attaching them to lifecycle owners if the work you are handling doesn’t involve the UI at all.
- Backpressure
The term backpressure refers to the scenario in which observables or producers emit too many values that cannot be properly consumed by attached observers. A proper backpressure strategy will be needed in order to handle emitted but unconsumed events.
Kotlin Flow
According to the Android docs, a flow is
In coroutines, a flow is a type that can emit multiple values sequentially, as opposed to suspend functions that return only a single value. For example, you can use a flow to receive live updates from a database.
At a glance, this actually sounds very similar to what LiveData objects aim to accomplish, and it is. Under the hood however, flows are a more rich and flexible API that expands upon the capabilities of LiveData, without some of the drawbacks. Let’s explore further:
Kotlin Flow Examples
Using our example from earlier, let’s see how we might approach this using Kotlin Flow instead of LiveData.
First, our MovieItemViewModel might declare a Flow object that is tied to the list of movies like this:
class MoviesListViewModel : ViewModel() {
private val moviesApi: MoviesApi
val moviesList: Flow<List<Movie>> = flow {
while(true){
val latestMovies = moviesApi.fetchLatestMovies()
emit(latestMovies)
delay(200)
}
}
// Rest of the ViewModel...
}
Then, back in our Activity we might retrieve or collect the data passed to our flow object like this
class MovieActivity : AppCompatActivity() {
private val moviesListViewModel: MoviesListViewModel by viewModels()
private val mAdapter: MoviesListAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModelScope.launch {
// Trigger the flow and consume its elements using collect
moviesListViewModel.moviesList.collect { latestMovies ->
// Update UI with the list of the latest movies
}
}
}
Multiple Operators
One of the best parts of the Kotlin flow API is the use of what is known as intermediate operators to transform your data. We know that the producer is responsible for fetching the data (and storing it), and flows can be used to emit the data to interested consumers. What if you wanted to make changes to the data along the way?
Similar to RxJava’s functional stream operators, Kotlin flows have operators that allow us to sort, filter, and manipulate our data in a variety of ways. If we only wanted to display a list of movies that have a rating of 3 stars or higher, we might do something like this in our Activity
viewModelScope.launch {
moviesListViewModel.moviesList
.map{it.filter {movie -> movie.rating >= 3}}
.collect { filteredMovies ->
//Show filtered movie list
}
}
Kotlin Flows were designed with coroutines in mind. Since Flows emit their values inside of suspend functions, you might not need them if you’re trying to do something more straightforward like grabbing some data and making an update to the UI. It might not be worth adding extra complexity and an increased learning curve to your codebase if the component you’re building is fairly simple.
Flows are also not lifecycle dependent like LiveData objects are, and there’s much more flexibility in how data gets delivered when compared to LiveData.
It’s important to understand that neither of these tools are explicitly “better” than the other, but to note that both have their pros and cons depending on the needs of the app development team and what their goals are.