热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

LiveDatabeyondtheViewModel

LiveDatabeyondtheViewModel-这个系列我做了协程和Flow开发者的一系列文章的翻译,旨在了解当前协程、Flow、LiveData这样设计的原因,从设计者的角

这个系列我做了协程和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 = ...

  val repositoryResult = Transformations.switchMap(userManager.userID) { userID ->
     repository.getDataForUser(userID)
  }
}

switchMap内部使用的也是MediatorLiveData,所以熟悉它很重要,隐藏,当你想结合多个LiveData的来源时,你需要使用它。

One-to-many dependency — MediatorLiveData

MediatorLiveData允许你将一个或多个数据源添加到一个LiveData观察器中。

val liveData1: LiveData = ...
val liveData2: LiveData = ...

val result = MediatorLiveData()

result.addSource(liveData1) { value ->
    result.setValue(value)
}
result.addSource(liveData2) { value ->
    result.setValue(value)
}

这个例子来自官方文档,当任何一个数据来源发生变化时,都会更新结果。请注意,数据不是自动为你组合的,MediatorLiveData只是负责通知的工作。

为了在我们的示例应用程序中实现转换,我们需要将两个不同的LiveDatas合并成一个。

使用MediatorLiveData来组合数据的方法是在不同的方法中添加来源和设置值。

fun blogpostBoilerplateExample(newUser: String): LiveData {

    val liveData1 = userOnlineDataSource.getOnlineTime(newUser)
    val liveData2 = userCheckinsDataSource.getCheckins(newUser)

    val result = MediatorLiveData()

    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,
        checkinsResult: LiveData
): 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()

    fun loadDataForUser(userId: String): LiveData {
        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 {
        val result = MutableLiveData()
        result.value = dataSource.getOnlineTime(userId)
        return result
    }
}

如果你要在消费者之间共享一个LiveData实例之前,请仔细考虑。

MediatorLiveData smell: adding sources outside initialization

使用观察者模式比持有对视图的引用更安全(通常在MVP架构中你会这样做)。然而,这并不意味着你可以忘记泄漏的问题!

考虑一下这个数据源。

class SlowRandomNumberGenerator {
    private val rnd = Random()

    fun getNumber(): LiveData {
        val result = MutableLiveData()

        // 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()

/**
* *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

/**
 * 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()

val randomNumber: LiveData = 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  LiveData.combineAndCompute(other: LiveData, onChange: (A, B) -> T): MediatorLiveData {

   var source1emitted = false
   var source2emitted = false

   val result = MediatorLiveData()

   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 {
   if (newUser == null) {
       return MutableLiveData().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 欢迎大家访问


推荐阅读
  • 基于PgpoolII的PostgreSQL集群安装与配置教程
    本文介绍了基于PgpoolII的PostgreSQL集群的安装与配置教程。Pgpool-II是一个位于PostgreSQL服务器和PostgreSQL数据库客户端之间的中间件,提供了连接池、复制、负载均衡、缓存、看门狗、限制链接等功能,可以用于搭建高可用的PostgreSQL集群。文章详细介绍了通过yum安装Pgpool-II的步骤,并提供了相关的官方参考地址。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • SpringBoot uri统一权限管理的实现方法及步骤详解
    本文详细介绍了SpringBoot中实现uri统一权限管理的方法,包括表结构定义、自动统计URI并自动删除脏数据、程序启动加载等步骤。通过该方法可以提高系统的安全性,实现对系统任意接口的权限拦截验证。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • C# 7.0 新特性:基于Tuple的“多”返回值方法
    本文介绍了C# 7.0中基于Tuple的“多”返回值方法的使用。通过对C# 6.0及更早版本的做法进行回顾,提出了问题:如何使一个方法可返回多个返回值。然后详细介绍了C# 7.0中使用Tuple的写法,并给出了示例代码。最后,总结了该新特性的优点。 ... [详细]
  • 后台获取视图对应的字符串
    1.帮助类后台获取视图对应的字符串publicclassViewHelper{将View输出为字符串(注:不会执行对应的ac ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
author-avatar
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有