Most Android developers use LiveData as part of the official Android Ecosystem. However, it’s not the only option for application state management in Android, and it happens that developers struggle with LiveData when complexity arises in the app.
In this article, you will learn what RxJava is, what LiveData is, how they differ, and why to use both in the project. We’ll go through a small example app with the following technology stack:
- MVVM
- LiveData
- RxJava/RxKotlin
- Dagger Hilt
Onwards, all the code examples will be written in the Kotlin language.
What is RxJava/RxKotlin?
RxJava implements ReactiveX, an API that deals with asynchronous data streams and the propagation of data changes. In RxJava, streams are a fundamental concept and represent sequences of data that can be observed and reacted upon. Streams are implemented using various types of Observables such as:
Using operators, such as map or filter, we can apply transformations to Rx streams, with each operator producing a new Observable that represents the transformed data stream. For example, we have an Observable representing a stream of user inputs. We could apply a filter operator to that Observable to only emit events when a certain condition is met, and then apply a map operator to transform the input events into a different format. The output of these operations would be a new Observable that represents the filtered and transformed stream of user inputs.
At first, it all may seem difficult. Still, once you understand the basics RxJava will allow you to handle complex asynchronous workflows, such as network requests, user input, and database queries, in a way that is easy to understand, maintain, and test.
What is LiveData?
LiveData is an observable data holder. Its purpose is to store and manage data related to the user interface and ensure that all updates to this data are always delivered to the UI components. LiveData simplifies the management of observers and automatically handles lifecycle events, such as starting and stopping observation based on the lifecycle state of the associated component (e.g., Activity or Fragment). Moreover, LiveData delivers updates to the UI only when the associated component is active. This helps prevent potential resource leaks and crashes.
RxJava vs LiveData
Both RxJava and LiveData provide similar functionality, such as observing changes to a data source and updating the UI accordingly. However, there are some key differences:
RxJava | LiveData | |
Complexity | Has a steeper learning curve, because it provides a lot of operators and abstractions to manipulate streams of data | It’s easy to learn and use |
Data Transformation | Provides a more powerful set of operators to transform data streams | Has fewer operators, but it is tightly integrated with the Android lifecycle, which makes it easier to use for simple use cases. |
Thread Management | Allows more control over threading, making it easier to perform complex asynchronous operations | Only runs on the main thread, which is good for UI updates and other simple use cases, but can be limiting in more complex scenarios. |
Architecture | Not tied to any specific architectural pattern | Is part of the Android Architecture Components and is designed to work with the MVVM architecture. |
The choice between the two depends on the specific use case and the developer’s preference for implementation and threading model. However, in the example app I will show how well RxJava and LiveData complement each other.
Benefits of using RxJava/RxKotlin + LiveData
App complexity
RxJava introduces more complexity due to its rich set of operators and concepts for handling asynchronous and event-driven programming. LiveData, on the other hand, simplifies the UI integration and offers lifecycle-aware observation. Combining both libraries can provide a balance between flexibility and simplicity, but it requires careful planning to manage complexity effectively. Developers should consider the specific requirements of their app when deciding on the appropriate usage of RxJava and LiveData.
Testability
Using RxJava and LiveData together, you can separate different parts of your application into independent units, making it easier to test and maintain your code.
You can write unit tests for your RxJava operators and transformations to ensure that your data processing logic is correct. Additionally, LiveData’s ability to propagate data changes in a predictable manner makes it easy to test UI components for data updates.
Thread Management
RxJava excels at handling asynchronous and concurrent operations, making it ideal for scenarios such as network requests, database operations, and background processing. LiveData, on the other hand, is designed for the lifecycle-aware observation of data changes. By combining the two, you can benefit from both the asynchronous nature of RxJava and the lifecycle awareness of LiveData.
Flexibility
The combination of RxJava and LiveData provides flexibility and interoperability. You can easily convert RxJava observables and LiveData objects using the LiveDataReactiveStreams.fromPublisher()
and LiveDataReactiveStreams.toPublisher()
methods, allowing both libraries to be seamlessly integrated into your code base. This flexibility allows you to choose the best approach for different parts of your application, depending on their specific requirements.
All in all, the combination of RxJava and LiveData provides a powerful set of tools for building responsive Android applications. By leveraging the strengths of both libraries, developers can create highly reactive, testable, and flexible systems that are easy to read and maintain. However, the learning curve for these two technologies can be steep, and it might take time to get used to their concepts, operators, and patterns. Overall, while there is additional complexity in using RxJava and LiveData together, their benefits outweigh the added complexity of managing data in Android apps.
How to connect LiveData and RxKotlin streams – an app example
To see how to bind the two together, let’s walk through a simple application listing movies by their genre.
Here’s the set of requirements the application should satisfy:
- downloading the list of movies from the API
- downloading the list of movie genres from the API
- combining the list with movies and list with genres and displaying it in the UI
- filtering movies by genre
For starters, we have two files: movies.json
and genres.json
, that will serve as a static API for our application.
[
{
"uuid": "3dfa1889-5411-46e9-a1cc-b4be10d407ac",
"title": "The Green Mile",
"directors": [
"Frank Darabont"
],
"year": "1999",
"genres": [
"aba10904-ca41-11ed-afa1-0242ac120002"
]
},
...
]
[
{
"uuid": "7053ee0c-ca41-11ed-afa1-0242ac120002",
"name": "Action"
},
{
"uuid": "642f0f2e-ca43-11ed-afa1-0242ac120002",
"name": "Adventure"
},
...
]
We are downloading/reading those files from assets in MoviesDataSourceImpl
private fun readRawMovies() {
val fileString = getFileFromAssets(MOVIES_FILE)
val movies = gson.fromJson(fileString, Array<MovieResponse>::class.java)
rawMovies.onNext(movies.toList())
}
private fun readAllGenres() {
val fileString = getFileFromAssets(GENRES_FILE)
val genres = gson.fromJson(fileString, Array<Genre>::class.java)
allGenres.onNext(genres.toList())
}
where rawMovies
and allGenres
streams are declared in interface MoviesDataSource
as BehaviourSubject
s instances.
BehavourSubject
s are in short, a form of observable that emits the most recently emitted item by the source Observable and then continues to emit any further items emitted later (you can learn more about subjects here).
interface MoviesDataSource {
val rawMovies: BehaviorSubject<List<MovieResponse>>
val allMovies: Observable<List<Movie>>
val allGenres: BehaviorSubject<List<Genre>>
}
Now that we have movies and genres downloaded from the API we need to combine those two lists to be able to display the human-readable name of the genre for each movie as the Movie
object stores only UUIDs of the movie’s genres. To do this we will create a new stream called allMovies
that will take UUID, title, directors and year from the MovieReponse
object returned from rawMovies
stream and genres returned from allGenres
stream filter by UUID.
override val allMovies = rawMovies.map {
it.map {
Movie(
uuid = it.uuid,
title = it.title,
directors = it.directors,
year = it.year,
genres = getGenresByUUID(it.genres)
)
}
}
Where getGenresByUUID()
looks like this:
private fun getGenresByUUID(movieGenreUuids: List<UUID>): List<Genre> {
val genres = allGenres.value ?: emptyList()
return movieGenreUuids.flatMap { genreUuid ->
genres.filter { it.uuid == genreUuid }
}
}
And now we have all data streams ready to use in the UI layer from the repository.
class MoviesRepository(dataSource: MoviesDataSource) {
val allMovies = dataSource.allMovies
val allGenres = dataSource.allGenres
}
This app implements the extended MVVM design pattern. Extended MVVM differs from MVVM by a MovieConnector
class that holds all the business logic from the ViewModel and allows you to write unit tests for the entire ViewModel without Android dependencies. If you have a lot of stream transformations, it’s worth testing them to ensure they work as expected. So the whole ViewModel will look like this:
@HiltViewModel
class MoviesViewModel @Inject constructor(
private val repository: MoviesRepository,
) : IMoviesConnector by MoviesConnector(repository), ViewModel()
where MoviesViewModel
delegates responsibility to MoviesConnector
to implement certain methods. To see how you can write unit tests for ViewModel check out this example.
Continuing our original task of listing movies in the UI, in MoviesConnector
we have onShowAllMovies
stream
class MoviesConnector(private val repository: MoviesRepository) : IMoviesConnector {
override val genreFilters = defaultBehavior<List<Genre>>(emptyList())
override val onShowAllMovies =
Observables.combineLatest(repository.allMovies, genreFilters).map { (movies, genres) ->
filterMoviesByGenre(movies, genres)
}
override val onFiltersButtonClick = publishSubject<Unit>()
override val onShowFilters =
onFiltersButtonClick.withLatestFrom(repository.allGenres, genreFilters)
.map { (_, genres, filters) ->
genres.map {
Pair(it, filters.contains(it))
}
}.filter { it.isNotEmpty() }
override fun onApplyFilters(genres: List<Genre>) = genreFilters.onNext(genres)
private fun filterMoviesByGenre(movies: List<Movie>, genres: List<Genre>) =
movies.filter { movie ->
if (genres.isNotEmpty()) {
genres.any { genre ->
movie.genres.any { it.uuid == genre.uuid }
}
} else {
true
}
}
}
that uses combineLatest operator, which returns the item emitted by each Observable when an item is emitted by either of allMovies
or genreFilters
.
So when the user selects any genre they want to filter movies by, it will be added to genreFilters
stream and filtered in onShowAllMovies
stream. So all that’s left is to display everything to the user.
To transform Rx streams into LiveData we will use LiveDataReactiveStreams, a class provided as part of Google’s Jetpack components. It provides a fromPublisher()
method that converts a ReactiveStreams (Publisher) into a LiveData object.
Here are some Kotlin extensions that will help with this conversion:
fun <T : Any> fromPublisher(observable: Observable<T>) =
LiveDataReactiveStreams.fromPublisher(
observable.toFlowable(BackpressureStrategy.LATEST)
)
fun <T : Any> Observable<T>.toLiveData() = fromPublisher(this)
fun <T : Any> Observable<T>.observeAsLiveData(owner: LifecycleOwner, block: (T) -> Unit) =
toLiveData().observeValue(owner) {
if (it != null) {
block(it)
}
}
fun <T : Any> connect(owner: LifecycleOwner, observable: Observable<T>, fn: (T) -> Unit) {
observable.observeAsLiveData(owner, fn)
}
fun <T> LiveData<T>.observeValue(owner: LifecycleOwner, block: (T) -> Unit) =
observe(owner) {
if (it != null) {
block(it)
}
}
in our MoviesActivity
, all we have to do is use connect()
extension like this:
private fun setupObservers() {
connect(this, moviesViewModel.onShowAllMovies, ::onShowAllMovies)
connect(this, moviesViewModel.onShowFilters, ::onShowFilters)
connect(this, filtersDialog.onApplyFilters, moviesViewModel::onApplyFilters)
}
private fun onShowFilters(value: List<Pair<Genre, Boolean>>) {
filtersDialog.setFilters(value)
filtersDialog.show(supportFragmentManager, "filters")
}
private fun onShowAllMovies(movies: List<Movie>) {
moviesAdapter.setData(movies)
}
where we pass three arguments:
- this (
MoviesActivity
) as LifeCycleOwner - observable streams that we want to convert to LiveData
- function to be called on data change
and that’s it, we combined RxKotlin with LiveData in a single application.
To see the whole code check out this repository.
Summary
To sum up, RxJava and LiveData, although similar, have some significant differences.
LiveData is easy to learn and considers the Android lifecycle, but it has few operators and only runs on the main thread, which can be limiting in more complex scenarios.
On the other hand, RxJava allows more control over threading and provides many operators to transform streams but is not lifecycle-aware and it might seem hard to learn at first.
So, why not use both? RxJava will do great in the data source and repository layers while LiveData will be responsible for UI updates in activity/fragment.
Although you can restrict the usage to only LiveData for smaller applications or only RxJava for more complex ones, in most cases a combination of both will work without having to worry about whether the application will grow in the future.
References
- https://reactivex.io/
- https://github.com/ReactiveX/RxJava
- https://developer.android.com/
- https://kotlinlang.org/
Elżbieta Hofman is an accomplished Android Developer at Makimo, where she navigates the intricacies of mobile development with a keen interest in Android and Kotlin programming. Passionately considering programming as an art form, she balances technical expertise with a thoughtful eye for aesthetics - both in code and user interface. She shares her wisdom via her blog and LinkedIn, fostering meaningful conversations around team dynamics and innovative solutions.