In this series, I translated a series of articles of coroutines and Flow developers, aiming to understand the reasons for the current design of coroutines, flows and LiveData, find out their problems from the perspective of designers, and how to solve these problems. PLS Enjoy it.
Reactive architecture has been a hot topic on Android for years. It has been a constant theme at Android conferences, often demonstrated with examples of RxJava (see the Rx section at the bottom). Reactive programming is a paradigm that focuses on “how the data flows” and “how it propagates.” It makes it easier to build application code and display data from asynchronous operations.
One tool that implements some of the reactive concepts is LiveData. It is a simple observer, aware of the life cycle of the observer. Exposing LiveData from your data source or repository is an easy way to make your architecture more reactive, but there are some potential pitfalls.
This post will help you avoid pitfalls and use patterns to help you build a more “reactive” architecture with LiveData.
LiveData ‘s purpose
In Android, activities, fragments, and views can be destroyed at almost any time, so any reference to one of these components can result in a leak or NullPointerException.
LiveData is designed to implement observer mode, allowing communication between view controllers (activities, fragments, etc.) and the source of UI data (usually the ViewModel).
With LiveData, this communication is more secure: due to its lifecycle awareness, data is received only when the View is in Activity state.
In short, the advantage is that you don’t need to manually unsubscribe between the View and the ViewModel.
LiveData beyond the ViewModel
The observable paradigm works very well between view controllers and ViewModels, so you can use it to observe other components of your application and take advantage of lifecycle awareness. Take these scenarios for example:
- Observe changes in SharedPreferences
- Observe a document or collection in Firestore
- Use an authentication SDK such as FirebaseAuth to observe the current user’s authorization
- Observe the query in Room (which supports LiveData out of the box)
The advantage of this mode is that since everything is connected, the user interface updates automatically when the data changes.
On the downside, LiveData does not provide a toolkit for composing data streams or managing threads, as Rx does.
If you used LiveData in each layer of a typical application, it would look something like this.
To pass data between components, we need a way to map and combine data. MediatorLiveData is a tool provided by LiveData to combine and perform Transformations.
- Transformations.map
- Transformations.switchMap
Note that when your View is destroyed you don’t need to destroy these subscriptions because the View’s lifecycle will be propagated downstream to continue the subscription.
Patterns
One-to-one static transformation — map
In our example above, the ViewModel simply forwards data from the repository to the view, transforming it into a UI model. Whenever there is new data in the repository, the ViewModel simply maps it.
class MainViewModel {
val viewModelResult = Transformations.map(repository.getDataForUser()) { data ->
convertDataToMainUIModel(data)
}
}
Copy the code
The transition is simple. However, if the User data above can be changed, then you need to use switchMap.
One-to-one dynamic transformation — switchMap
Consider this example: you are watching a User manager that exposes users, and you need to get their IDS before you can observe the repository.
You cannot create them during initialization of the ViewModel because the user ID is not immediately available. You can do this with switchMap.
class MainViewModel {
// val userId: LiveData<String> = ...
val repositoryResult = Transformations.switchMap(userManager.userID) { userID ->
repository.getDataForUser(userID)
}
}
Copy the code
SwitchMap also uses MediatorLiveData internally, so it is important to be familiar with it, hidden, and you need to use it when you want to combine multiple LiveData sources.
One – to – many dependency – MediatorLiveData
MediatorLiveData allows you to add one or more data sources to a LiveData observer.
val liveData1: LiveData<Int> = ...
val liveData2: LiveData<Int> = ...
val result = MediatorLiveData<Int>()
result.addSource(liveData1) { value ->
result.setValue(value)
}
result.addSource(liveData2) { value ->
result.setValue(value)
}
Copy the code
This example comes from official documentation, and the results are updated when any of the data sources change. Please note that the data is not automatically assembled for you, MediatorLiveData is only responsible for notification.
To implement the transformation in our sample application, we need to merge two different LiveDatas into one.
MediatorLiveData is used to combine data by adding sources and setting values in different methods.
fun blogpostBoilerplateExample(newUser: String): LiveData<UserDataResult> {
val liveData1 = userOnlineDataSource.getOnlineTime(newUser)
val liveData2 = userCheckinsDataSource.getCheckins(newUser)
val result = MediatorLiveData<UserDataResult>()
result.addSource(liveData1) { value ->
result.value = combineLatestData(liveData1, liveData2)
}
result.addSource(liveData2) { value ->
result.value = combineLatestData(liveData1, liveData2)
}
return result
}
Copy the code
The actual composition of the data is done in the combineLatestData method.
private fun combineLatestData(
onlineTimeResult: LiveData<Long>,
checkinsResult: LiveData<CheckinsResult>
): UserDataResult {
val onlineTime = onlineTimeResult.value
val checkins = checkinsResult.value
// Don't send a success until we have both results
if (onlineTime == null || checkins == null) {
return UserDataLoading()
}
// TODO: Check for errors and return UserDataError if any.
return UserDataSuccess(timeOnline = onlineTime, checkins = checkins)
}
Copy the code
It checks if the value is ready or correct and issues a result (load, error, or success).
When not to use LiveData
Even if you want to try “reaction formulas”, you need to understand the advantages of LiveData before adding it everywhere. If a component of your application has no connection to the user interface, it may not need LiveData.
For example, a user manager in your application might listen for changes in your authentication provider (such as Firebase Auth) and upload a unique token to your server.
The token uploader can watch the user manager, but with whose lifecycle? This operation has nothing to do with the View at all. Also, if the View is destroyed, the user token may never be uploaded.
Another option is to use the token uploader’s observeForever() and somehow hook into the user manager’s life cycle and delete the subscription when it’s done.
However, you don’t have to make everything visible. In this scenario, you can have the user manager call the token uploader directly (or whatever makes sense for your architecture).
If part of your application doesn’t affect the user interface, you probably don’t need LiveData.
Antipattern: Sharing instances of LiveData
When one class exposes one LiveData to another class, think carefully about whether you want to expose the same Or different instances of LiveData.
class SharedLiveDataSource(val dataSource: MyDataSource) {
// Caution: this LiveData is shared across consumers
private val result = MutableLiveData<Long>()
fun loadDataForUser(userId: String): LiveData<Long> {
result.value = dataSource.getOnlineTime(userId)
return result
}
}
Copy the code
If this class is a singleton (with only one instance) in your application, you can always return the same LiveData, right? Not necessarily: This class may have multiple consumers. For example, consider this scenario.
sharedLiveDataSource.loadDataForUser("1").observe(this, Observer {
// Show result on screen
})
Copy the code
And the second consumer is using it.
sharedLiveDataSource.loadDataForUser("2").observe(this, Observer {
// Show result on screen
})
Copy the code
The first consumer will receive an update of the data belonging to user “2 “.
Even if you think you are only using the class from a consumer, you may end up with an error by using this pattern. For example, when navigating from one instance of an Activity to another, the new instance might temporarily receive data from the previous instance. Remember that LiveData dispatches the latest value to the new observer. In addition, Activity transitions are introduced in Lollipop, and they introduce an interesting edge case: two activities are active. This means that the sole consumer of LiveData may have two instances, one of which may display incorrect data.
The solution to this problem is to return a new LiveData for each consumer.
class SharedLiveDataSource(val dataSource: MyDataSource) {
fun loadDataForUser(userId: String): LiveData<Long> {
val result = MutableLiveData<Long>()
result.value = dataSource.getOnlineTime(userId)
return result
}
}
Copy the code
Think carefully before sharing an instance of LiveData between consumers.
MediatorLiveData smell: adding sources outside initialization
It is safer to use observer mode than to hold references to views (which you usually do in MVP architectures). However, that doesn’t mean you can forget about leaks!
Consider this data source.
class SlowRandomNumberGenerator {
private val rnd = Random()
fun getNumber(): LiveData<Int> {
val result = MutableLiveData<Int>()
// Send a random number after a while
Executors.newSingleThreadExecutor().execute {
Thread.sleep(500)
result.postValue(rnd.nextInt(1000))
}
return result
}
}
Copy the code
It simply returns a new LiveData with a random value after 500ms. There’s nothing wrong with that.
In the ViewModel, we need to expose a randomNumber property to get the number from the generator. Using MediatorLiveData for this is not ideal because it requires you to add a source every time you need a new number.
val randomNumber = MediatorLiveData<Int>()
/**
* *Don't do this.*
*
* Called when the user clicks on a button
*
* This function adds a new source to the result but it doesn't remove the previous ones.
*/
fun onGetNumber() {
randomNumber.addSource(numberGenerator.getNumber()) {
randomNumber.value = it
}
}
Copy the code
If we add a source to MediatorLiveData every time the user clicks the button, the application will work as expected. However, we are leaking all previous LiveDatas and these LiveDatas will no longer send updates, so it is a waste.
You can store a reference to a source and then remove it before adding a new source. (Spoiler: this is what Transformations.switchMap does! See solution below.)
Instead of using MediatorLiveData, we tried (and failed) to fix the problem with Transformation.map.
Transformation smell: Transformations outside initialization
Using the previous example, this is not feasible.
var lateinit randomNumber: LiveData<Int>
/**
* Called on button click.
*/
fun onGetNumber() {
randomNumber = Transformations.map(numberGenerator.getNumber()) {
it
}
}
Copy the code
There is an important point to understand here. The transform creates a new LiveData (both map and switchMap) when called. In this example, a randomNumber is exposed in the view, but it is reassigned every time the user clicks the button. It is very common for observers to only receive updates to LiveData assigned to var at subscription time.
viewmodel.randomNumber.observe(this, Observer { number ->
numberTv.text = resources.getString(R.string.random_text, number)
})
Copy the code
This subscription occurs in onCreate(), so if the viewModel.randomNumber LiveData instance changes later, the observer will not be called again.
In other words. Do not use Livedata in var. At initialization, the contents of the transformation are written.
Solution: wire transformations during initialization
Initialize exposed LiveData as a Transformation.
private val newNumberEvent = MutableLiveData<Event<Any>>()
val randomNumber: LiveData<Int> = Transformations.switchMap(newNumberEvent) {
numberGenerator.getNumber()
}
Copy the code
Use an event in LiveData to indicate when a new number is requested.
/**
* Notifies the event LiveData of a new request for a random number.
*/
fun onGetNumber() {
newNumberEvent.value = Event(Unit)
}
Copy the code
If you’re not familiar with this pattern, take a look at this article on Activities.
Medium.com/androiddeve…
Bonus section
Tidying up with Kotlin
The MediatorLiveData example above shows some code duplication, so we can take advantage of Kotlin’s extension function.
/** * Sets the value to the result of a function that is called when both `LiveData`s have data * or when they receive updates after that. */ fun <T, A, B> LiveData<A>.combineAndCompute(other: LiveData<B>, onChange: (A, B) -> T): MediatorLiveData<T> { var source1emitted = false var source2emitted = false val result = MediatorLiveData<T>() val mergeF = { val source1Value = this.value val source2Value = other.value if (source1emitted && source2emitted) { result.value = onChange.invoke(source1Value!! , source2Value!! ) } } result.addSource(this) { source1emitted = true; mergeF.invoke() } result.addSource(other) { source2emitted = true; mergeF.invoke() } return result }Copy the code
The repository now looks much cleaner.
fun getDataForUser(newUser: String?) : LiveData<UserDataResult> { if (newUser == null) { return MutableLiveData<UserDataResult>().apply { value = null } } return userOnlineDataSource.getOnlineTime(newUser) .combineAndCompute(userCheckinsDataSource.getCheckins(newUser)) { a, b -> UserDataSuccess(a, b) } }Copy the code
LiveData and RxJava
Finally, let’s discuss the obvious question that no one wants to discuss. LiveData is designed to allow views to observe the ViewModel. Make sure you use it for this! Even if you already use Rx, you can communicate with LiveData Active Reams.
If you want to use LiveData outside of the presentation layer, you may find that MediatorLiveData does not provide a toolkit to compose and manipulate data streams as RxJava does. However, Rx has a steep learning curve. A combination of LiveData transformations (and Kotlin magic) may be sufficient for your situation, but if you (and your team) have invested in learning RxJava, you probably don’t need LiveData.
If you use auto-dispose, using LiveData for this purpose would be redundant.
Original link: medium.com/androiddeve…
I would like to recommend my website xuyisheng. Top/focusing on Android-Kotlin-flutter welcome you to visit