LiveData beyond the ViewModel
点击上方蓝字关注我,知识会给你力量
这个系列我做了协程和Flow开发者的一系列文章的翻译,旨在了解当前协程、Flow、LiveData这样设计的原因,从设计者的角度,发现他们的问题,以及如何解决这些问题,pls enjoy it。
多年来,反应式架构一直是Android的一个热门话题。它一直是Android会议上的一个永恒主题,通常都是用RxJava的例子来进行演示的(见底部的Rx部分)。反应式编程是一种关注数据「如何流动」以及「如何传播」的范式,它可以简化构建应用程序的代码,方便显示来自异步操作的数据。
实现一些反应式概念的一个工具是LiveData。它是一个简单的观察者,能够意识到观察者的生命周期。从你的数据源或存储库中暴露LiveData是使你的架构更具反应性的一个简单方法,但也有一些潜在的陷阱。
这篇博文将帮助你避免陷阱,并使用一些模式来帮助你使用LiveData构建一个更加「反应式」的架构。
LiveData’s purpose
在Android中,Activity、Fragment和视图几乎可以在任何时候被销毁,所以对这些组件之一的任何引用都可能导致泄漏或NullPointerException异常。
LiveData被设计用来实现观察者模式,允许视图控制器(Activity、Fragment等)和UI数据的来源(通常是ViewModel)之间进行通信。
通过LiveData,这种通信更加安全:由于它的生命周期意识,数据只有在View处于Activity状态时才会被接收。
简而言之,其优点是你不需要在View和ViewModel之间手动取消订阅。
LiveData beyond the ViewModel
可观察范式在视图控制器和ViewModel之间工作得非常好,所以你可以用它来观察你的应用程序的其他组件,并利用生命周期意识的优势。比如说下面这些场景:
-
观察SharedPreferences中的变化 -
观察Firestore中的一个文档或集合 -
用FirebaseAuth这样的认证SDK观察当前用户的授权 -
观察Room中的查询(它支持开箱即用的LiveData)
这种模式的优点是,由于所有的东西都是连在一起的,所以当数据发生变化时,用户界面会自动更新。
缺点是,LiveData并没有像Rx那样提供一个用于组合数据流或管理线程的工具包。
如果在一个典型的应用程序的每一层中使用LiveData,看起来就像这样。
为了在组件之间传递数据,我们需要一种方法来映射和组合数据。MediatorLiveData就是LiveData提供的用于组合数据的工具,同时与Transformations类也提供了一些变换工具。
-
Transformations.map -
Transformations.switchMap
请注意,当你的View被销毁时,你不需要销毁这些订阅,因为View的lifecycle会被传播到下游后继续订阅。
Patterns
One-to-one static transformation — map
在我们上面的例子中,ViewModel只是将数据从资源库转发到视图,将其转换为UI模型。每当资源库有新的数据时,ViewModel只需对其进行映射即可。
class MainViewModel {
val viewModelResult = Transformations.map(repository.getDataForUser()) { data ->
convertDataToMainUIModel(data)
}
}
这种转变是非常简单的。然而,如果上面的User数据是可以改变的,那么你需要使用switchMap。
One-to-one dynamic transformation — switchMap
考虑一下这个例子:你正在观察一个暴露了User的用户管理器,你需要获取他们的ID,然后才能对存储库进行观察。
你不能在ViewModel的初始化中创建它们,因为用户ID不是立即可用的。你可以用switchMap来实现这一点。
class MainViewModel {
// val userId: LiveData<String> = ...
val repositoryResult = Transformations.switchMap(userManager.userID) { userID ->
repository.getDataForUser(userID)
}
}
switchMap内部使用的也是MediatorLiveData,所以熟悉它很重要,隐藏,当你想结合多个LiveData的来源时,你需要使用它。
One-to-many dependency — MediatorLiveData
MediatorLiveData允许你将一个或多个数据源添加到一个LiveData观察器中。
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)
}
这个例子来自官方文档,当任何一个数据来源发生变化时,都会更新结果。请注意,数据不是自动为你组合的,MediatorLiveData只是负责通知的工作。
为了在我们的示例应用程序中实现转换,我们需要将两个不同的LiveDatas合并成一个。
使用MediatorLiveData来组合数据的方法是在不同的方法中添加来源和设置值。
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
}
数据的实际组合是在combineLatestData方法中完成的。
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)
}
它检查值是否准备好或正确,并发出一个结果(加载、错误或成功)。
When not to use LiveData
即使你想尝试"反应式",你也需要在到处添加LiveData之前了解其优势。如果你的应用程序的某个组件与用户界面没有任何联系,它可能不需要LiveData。
例如,你应用中的一个用户管理器会监听你的认证提供者(如Firebase Auth)的变化,并向你的服务器上传一个唯一的令牌。
令牌上传者可以观察用户管理器,但用谁的生命周期?这个操作与View完全没有关系。此外,如果View被销毁,用户令牌可能永远不会被上传。
另一个选择是使用令牌上传器的observeForever(),并以某种方式钩住用户管理器的生命周期,在完成后删除订阅。
然而,你不需要让所有的东西都能被观察到。这个场景下,你可以让用户管理器直接调用令牌上传器(或任何对你的架构有意义的东西)。
❝如果你的应用程序的一部分不影响用户界面,你可能不需要LiveData。
❞
Antipattern: Sharing instances of LiveData
当一个类将一个LiveData暴露给其他类时,请仔细考虑是否要暴露同一个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
}
}
如果这个类在你的应用程序中是一个单例(只有一个实例),你就可以总是返回同一个LiveData,对吗?不一定:这个类可能有多个消费者。例如,考虑这个场景。
sharedLiveDataSource.loadDataForUser("1").observe(this, Observer {
// Show result on screen
})
而第二个消费者也在使用它。
sharedLiveDataSource.loadDataForUser("2").observe(this, Observer {
// Show result on screen
})
第一个消费者将收到属于用户 "2 "的数据的更新。
即使你认为你只是从一个消费者那里使用这个类,你也可能因为使用这种模式而最终出现错误。例如,当从一个Activity的一个实例导航到另一个实例时,新的实例可能会暂时收到来自前一个实例的数据。请记住,LiveData会将最新的值分派给新的观察者。另外,Lollipop中引入了Activity转换,它们带来了一个有趣的边缘情况:两个Activity处于活动状态。这意味着LiveData的唯一消费者可能有两个实例,其中一个可能会显示错误的数据。
解决这个问题的方法是为每个消费者返回一个新的LiveData。
class SharedLiveDataSource(val dataSource: MyDataSource) {
fun loadDataForUser(userId: String): LiveData<Long> {
val result = MutableLiveData<Long>()
result.value = dataSource.getOnlineTime(userId)
return result
}
}
如果你要在消费者之间共享一个LiveData实例之前,请仔细考虑。
MediatorLiveData smell: adding sources outside initialization
使用观察者模式比持有对视图的引用更安全(通常在MVP架构中你会这样做)。然而,这并不意味着你可以忘记泄漏的问题!
考虑一下这个数据源。
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
}
}
它只是在500ms后返回一个带有随机值的新LiveData。这并没有什么问题。
在ViewModel中,我们需要公开一个randomNumber属性,从生成器中获取数字。为此使用MediatorLiveData并不理想,因为它要求你在每次需要新数字时都要添加源。
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
}
}
如果每次用户点击按钮时,我们都向MediatorLiveData添加一个源,那么该应用就能按预期工作。然而,我们正在泄露所有以前的LiveDatas,这些LiveDatas不会再发送更新,所以这是一种浪费。
你可以存储一个对源的引用,然后在添加新的源之前将其删除。(Spoiler: this is what Transformations.switchMap does! See solution below.)
我们不要使用MediatorLiveData,而是尝试(但失败了)用Transformation.map来解决这个问题。
Transformation smell: Transformations outside initialization
使用前面的例子,这就不可行了。
var lateinit randomNumber: LiveData<Int>
/**
* Called on button click.
*/
fun onGetNumber() {
randomNumber = Transformations.map(numberGenerator.getNumber()) {
it
}
}
这里有一个重要的问题需要理解。变换在调用时创建一个新的LiveData(包括map和switchMap)。在这个例子中,随机数(randomNumber)被暴露在视图中,但每次用户点击按钮时它都会被重新分配。观察者只在订阅的时候接收分配给var的LiveData的更新,这是非常常见的。
viewmodel.randomNumber.observe(this, Observer { number ->
numberTv.text = resources.getString(R.string.random_text, number)
})
这个订阅发生在onCreate()中,所以如果之后viewmodel.randomNumber LiveData实例发生变化,观察者将不会被再次调用。
换句话说。不要在var中使用Livedata。在初始化的时候,要将转换的内容写入。
Solution: wire transformations during initialization
将暴露的LiveData初始化为一个transformation。
private val newNumberEvent = MutableLiveData<Event<Any>>()
val randomNumber: LiveData<Int> = Transformations.switchMap(newNumberEvent) {
numberGenerator.getNumber()
}
在LiveData中使用一个事件来指示何时请求一个新号码。
/**
* Notifies the event LiveData of a new request for a random number.
*/
fun onGetNumber() {
newNumberEvent.value = Event(Unit)
}
如果你不熟悉这种模式,请看这篇关于Activity的文章。
https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
Bonus section
Tidying up with Kotlin
上面的MediatorLiveData例子显示了一些代码的重复,所以我们可以利用Kotlin的扩展函数。
/**
* 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
}
存储库现在看起来干净多了。
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)
}
}
LiveData and RxJava
最后,让我们来讨论一个显而易见而又没人愿意讨论的问题。LiveData被设计为允许视图观察ViewModel。一定要把它用在这上面! 即使你已经使用了Rx,你也可以用LiveDataReactiveStreams进行通信。
如果你想在表现层之外使用LiveData,你可能会发现MediatorLiveData并没有像RxJava那样提供一个工具包来组合和操作数据流。然而,Rx有一个陡峭的学习曲线。LiveData转换(和Kotlin魔法)的组合可能足以满足你的情况,但如果你(和你的团队)已经投资学习RxJava,你可能不需要LiveData。
如果你使用auto-dispose,那么为此使用LiveData将是多余的。
原文链接:https://medium.com/androiddevelopers/livedata-beyond-the-viewmodel-reactive-patterns-using-transformations-and-mediatorlivedata-fda520ba00b7
向大家推荐下我的网站 https://xuyisheng.top/ 专注 Android-Kotlin-Flutter 欢迎大家访问
向大家推荐下我的网站 https://xuyisheng.top/ 点击原文一键直达
专注 Android-Kotlin-Flutter 欢迎大家访问
往期推荐
更文不易,点个“三连”支持一下👇