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之间手动取消订阅。

img

LiveData beyond the ViewModel

可观察范式在视图控制器和ViewModel之间工作得非常好,所以你可以用它来观察你的应用程序的其他组件,并利用生命周期意识的优势。比如说下面这些场景:

  • 观察SharedPreferences中的变化
  • 观察Firestore中的一个文档或集合
  • 用FirebaseAuth这样的认证SDK观察当前用户的授权
  • 观察Room中的查询(它支持开箱即用的LiveData)

这种模式的优点是,由于所有的东西都是连在一起的,所以当数据发生变化时,用户界面会自动更新。

缺点是,LiveData并没有像Rx那样提供一个用于组合数据流或管理线程的工具包。

如果在一个典型的应用程序的每一层中使用LiveData,看起来就像这样。

img

为了在组件之间传递数据,我们需要一种方法来映射和组合数据。MediatorLiveData就是LiveData提供的用于组合数据的工具,同时与Transformations类也提供了一些变换工具。

  • Transformations.map
  • Transformations.switchMap

请注意,当你的View被销毁时,你不需要销毁这些订阅,因为View的lifecycle会被传播到下游后继续订阅。

Patterns

One-to-one static transformation — map

img

在我们上面的例子中,ViewModel只是将数据从资源库转发到视图,将其转换为UI模型。每当资源库有新的数据时,ViewModel只需对其进行映射即可。

class MainViewModel {
  val viewModelResult = Transformations.map(repository.getDataForUser()) { data ->
     convertDataToMainUIModel(data)
  }
}
复制代码

这种转变是非常简单的。然而,如果上面的User数据是可以改变的,那么你需要使用switchMap。

One-to-one dynamic transformation — switchMap

考虑一下这个例子:你正在观察一个暴露了User的用户管理器,你需要获取他们的ID,然后才能对存储库进行观察。

img

你不能在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合并成一个。

img

使用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)的变化,并向你的服务器上传一个唯一的令牌。

img

令牌上传者可以观察用户管理器,但用谁的生命周期?这个操作与View完全没有关系。此外,如果View被销毁,用户令牌可能永远不会被上传。

另一个选择是使用令牌上传器的observeForever(),并以某种方式钩住用户管理器的生命周期,在完成后删除订阅。

然而,你不需要让所有的东西都能被观察到。这个场景下,你可以让用户管理器直接调用令牌上传器(或任何对你的架构有意义的东西)。

img

如果你的应用程序的一部分不影响用户界面,你可能不需要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的文章。

medium.com/androiddeve…

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将是多余的。

原文链接:medium.com/androiddeve…

向大家推荐下我的网站 xuyisheng.top/ 专注 Android-Kotlin-Flutter 欢迎大家访问

猜你喜欢

转载自juejin.im/post/7039885021099868174