如何一个接一个地合并两个实时数据?

Posted

技术标签:

【中文标题】如何一个接一个地合并两个实时数据?【英文标题】:How to combine two live data one after the other? 【发布时间】:2018-11-09 00:32:21 【问题描述】:

我有下一个用例:用户来到注册表,输入姓名、电子邮件和密码,然后单击注册按钮。在该系统需要检查电子邮件是否被接收并基于该显示错误消息或创建新用户之后......

我正在尝试使用 Room、ViewModel 和 LiveData 来做到这一点。这是我尝试学习这些组件的一些项目,我没有远程 api,我会将所有内容存储在本地数据库中

所以我有这些课程:

注册活动 注册ViewModel 用户 UsersDAO 用户存储库 UsersRegistrationService

所以我的想法是,注册按钮会附加一个监听器,它将调用RegisterViewModel::register() 方法。

class RegisterViewModel extends ViewModel 

    //...

    public void register() 
        validationErrorMessage.setValue(null);
        if(!validateInput())
            return;
        registrationService.performRegistration(name.get(), email.get(), password.get());
    

    //...


这就是基本的想法,我也想让performRegistration返回给我新创建的用户。

最困扰我的是我不知道如何在服务中实现performRegistration功能

class UsersRegistrationService 
    private UsersRepository usersRepo;

    //...

    public LiveData<RegistrationResponse<Parent>>  performRegistration(String name, String email, String password) 
         // 1. check if email exists using repository
         // 2. if user exists return RegistrationResponse.error("Email is taken") 
         // 3. if user does not exists create new user and return RegistrationResponse(newUser)
    

据我了解,UsersRepository 中的方法应返回 LiveData,因为 UsersDAO 正在返回 LiveData

@Dao
abstract class UsersDAO  
    @Query("SELECT * FROM users WHERE email = :email LIMIT 1")
    abstract LiveData<User> getUserByEmail(String email);


class UsersRepository 
    //...
    public LiveData<User> findUserByEmail(String email) 
        return this.usersDAO.getUserByEmail(email);
    

所以我的问题是如何实现performRegistration() 函数以及如何将值传递回视图模型,然后如何将活动从 RegisterActivity 更改为 MainActivity...

【问题讨论】:

那么performRegistration 基本上是一种插入方法?而且,并不是所有的 Dao 方法都应该返回 LiveData 是的,但它确实需要检查该电子邮件是否被接收 所以在插入之前你想查询数据库以检查电子邮件是否已经存在,对吧? 是的,但 DAO.getUserByEmail() 返回 LiveData 您应该查看架构组件指南developer.android.com/jetpack/docs/guide。在 UsersRegistrationService 类中,您需要一个 MediatorLivedata,您将作为源 LiveDatas 添加到每个注册用户的状态。 【参考方案1】:

你可以使用我的辅助方法:

val profile = MutableLiveData<ProfileData>()

val user = MutableLiveData<CurrentUser>()

val title = profile.combineWith(user)  profile, user ->
    "$profile.job $user.name"


fun <T, K, R> LiveData<T>.combineWith(
    liveData: LiveData<K>,
    block: (T?, K?) -> R
): LiveData<R> 
    val result = MediatorLiveData<R>()
    result.addSource(this) 
        result.value = block(this.value, liveData.value)
    
    result.addSource(liveData) 
        result.value = block(this.value, liveData.value)
    
    return result

【讨论】:

如此优雅简洁,听起来很邪恶:) 美丽的... :) 我试过这个,它不会等待两个 LiveData 准备好,即 A 为空,B 准备好,它仍然触发了观察者...有什么方法可以等待两者都准备好?【参考方案2】:

在MediatorLiveData 的帮助下,您可以合并来自多个来源的结果。这是我如何结合两个来源的示例:

class CombinedLiveData<T, K, S>(source1: LiveData<T>, source2: LiveData<K>, private val combine: (data1: T?, data2: K?) -> S) : MediatorLiveData<S>() 

    private var data1: T? = null
    private var data2: K? = null

    init 
        super.addSource(source1) 
            data1 = it
            value = combine(data1, data2)
        
        super.addSource(source2) 
            data2 = it
            value = combine(data1, data2)
        
    

    override fun <S : Any?> addSource(source: LiveData<S>, onChanged: Observer<in S>) 
        throw UnsupportedOperationException()
    

    override fun <T : Any?> removeSource(toRemove: LiveData<T>) 
        throw UnsupportedOperationException()
    

这是上面的要点,以防将来更新: https://gist.github.com/guness/0a96d80bc1fb969fa70a5448aa34c215

【讨论】:

为了编译它(至少在 Android Studio 3.4 下),我必须在 addSource() 的签名中的 T 类型参数之前添加“in”。 override fun &lt;T : Any?&gt; addSource(source: LiveData&lt;T&gt;, onChanged: Observer&lt;in T&gt;) 如果源计数是可变的而不是两个怎么办? 如何在 Java 中实现“组合”部分? 为什么需要unsupported operation 的两个覆盖? @EpicPandaForce 你不需要覆盖,但它们只是为了防止误用【参考方案3】:

Jose Alcérreca 可能有best answer for this:

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

【讨论】:

请注意,这不会为您合并结果。它只会在任一来源发生变化时通知您 @TheRealChx101 合并数据的方式在链接的文章中,基本上是combineLatestData函数【参考方案4】:

您可以定义一个方法,使用 MediatorLiveData 组合多个 LiveData,然后将该组合结果公开为一个元组。

public class CombinedLiveData2<A, B> extends MediatorLiveData<Pair<A, B>> 
    private A a;
    private B b;

    public CombinedLiveData2(LiveData<A> ld1, LiveData<B> ld2) 
        setValue(Pair.create(a, b));

        addSource(ld1, (a) ->  
             if(a != null) 
                this.a = a;
              
             setValue(Pair.create(a, b)); 
        );

        addSource(ld2, (b) ->  
            if(b != null) 
                this.b = b;
             
            setValue(Pair.create(a, b));
        );
    

如果您需要更多值,那么您可以创建一个CombinedLiveData3&lt;A,B,C&gt; 并公开一个Triple&lt;A,B,C&gt; 而不是Pair 等。就像在https://***.com/a/54292960/2413303 中一样。

编辑:嘿,我什至为你做了一个库,从 2 到 16:https://github.com/Zhuinden/livedata-combinetuple-kt

【讨论】:

我们可以做一个更通用的方法吗?例如对于 4 个实时数据,我们需要添加另一个类等等...... 如果您一次需要组合超过 6 个,我通常会感到惊讶。所以有 6 个类(+ 元组)还不错。 我来自未来,我有一个最多 11 元元组的组合器方法,谁知道呢? 我们如何在片段中实际使用这个类? @AndroidDev123 你不需要使用这个类,使用编辑中的库:github.com/Zhuinden/livedata-combinetuple-kt 现在你可以说combineTuple(liveData1, liveData2),它就可以工作了。 Kotlin 库。【参考方案5】:

我根据@guness 的回答做了一个方法。我发现仅限于两个LiveDatas 并不好。如果我们想使用 3 怎么办?我们需要为每种情况创建不同的类。所以,我创建了一个类来处理无限数量的LiveDatas。

/**
  * CombinedLiveData is a helper class to combine results from multiple LiveData sources.
  * @param liveDatas Variable number of LiveData arguments.
  * @param combine   Function reference that will be used to combine all LiveData data results.
  * @param R         The type of data returned after combining all LiveData data.
  * Usage:
  * CombinedLiveData<SomeType>(
  *     getLiveData1(),
  *     getLiveData2(),
  *     ... ,
  *     getLiveDataN()
  * )  datas: List<Any?> ->
  *     // Use datas[0], datas[1], ..., datas[N] to return a SomeType value
  * 
  */
 class CombinedLiveData<R>(vararg liveDatas: LiveData<*>,
                           private val combine: (datas: List<Any?>) -> R) : MediatorLiveData<R>() 

      private val datas: MutableList<Any?> = MutableList(liveDatas.size)  null 

      init 
         for(i in liveDatas.indices)
             super.addSource(liveDatas[i]) 
                 datas[i] = it
                 value = combine(datas)
             
         
     
 

【讨论】:

我收到了Type Mismatch Error, Required: R? Found: Unit value = combine(datas) 那是因为您的 combine 方法返回的是 Unit 而不是 R。请注意课程文档中的return a SomeType value TNX。它是有用的辅助类,但需要注意的是,combine 函数的返回类型应该与 CombinedLiveData 类中声明的 SomeType 相同, 泛型类型。例如,如果 SomeType 是布尔值? (可为空值)组合方法也必须返回布尔值?输入【参考方案6】:
LiveData liveData1 = ...;
 LiveData liveData2 = ...;

 MediatorLiveData liveDataMerger = new MediatorLiveData<>();
 liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value));
 liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value));

【讨论】:

不错的复制粘贴,但解释会很好【参考方案7】:

如果你希望两个值都不为空

fun <T, V, R> LiveData<T>.combineWithNotNull(
        liveData: LiveData<V>,
        block: (T, V) -> R
): LiveData<R> 
    val result = MediatorLiveData<R>()
    result.addSource(this) 
        this.value?.let  first ->
            liveData.value?.let  second ->
                result.value = block(first, second)
            
        
    
    result.addSource(liveData) 
        this.value?.let  first ->
            liveData.value?.let  second ->
                result.value = block(first, second)
            
        
    

    return result

【讨论】:

【参考方案8】:

没有自定义类

MediatorLiveData<Pair<Foo?, Bar?>>().apply 
    addSource(fooLiveData)  value = it to value?.second 
    addSource(barLiveData)  value = value?.first to it 
.observe(this)  pair ->
    // TODO

【讨论】:

【参考方案9】:

其中许多答案都有效,但也假定 LiveData 泛型类型不可为空。

但是,如果一个或多个给定输入类型是可空类型(假设 Kotlin 泛型的默认上限是 Any?,它是可空的)怎么办? 结果是即使 LiveData 发射器会发出一个值 (null),MediatorLiveData 也会忽略它,认为这是他自己的子实时数据值未设置。

相反,此解决方案通过强制传递给中介器的类型的上限不为空来处理它。懒惰但需要。

此外,此实现在调用组合器函数后避免了相同值,这可能是也可能不是您需要的,因此请随意删除那里的相等检查。

fun <T1 : Any, T2 : Any, R> combineLatest(
    liveData1: LiveData<T1>,
    liveData2: LiveData<T2>,
    combiner: (T1, T2) -> R,
): LiveData<R> = MediatorLiveData<R>().apply 
    var first: T1? = null
    var second: T2? = null

    fun updateValueIfNeeded() 
        value = combiner(
            first ?: return,
            second ?: return,
        )?.takeIf  it != value  ?: return
    

    addSource(liveData1) 
        first = it
        updateValueIfNeeded()
    
    addSource(liveData2) 
        second = it
        updateValueIfNeeded()
    

【讨论】:

【参考方案10】:

如果你想在构建时创建一个字段并设置(使用also):

val liveData1 = MutableLiveData(false)
val liveData2 = MutableLiveData(false)

// Return true if liveData1 && liveData2 are true
val liveDataCombined = MediatorLiveData<Boolean>().also 
    // Initial value
    it.value = false
    // Observing changes
    it.addSource(liveData1)  newValue ->
        it.value = newValue && liveData2.value!!
    
    it.addSource(selectedAddOn)  newValue ->
        it.value = liveData1.value!! && newValue
    

【讨论】:

以上是关于如何一个接一个地合并两个实时数据?的主要内容,如果未能解决你的问题,请参考以下文章

django,ajax:如何有效地实时更新一堆数据

合并复制可以以 0 毫秒的延迟运行,即实时吗?

如何将实时数据输入 NextJS?

如何在 PyQtGraph 的一个图中绘制两个实时数据?

技术干货|如何利用 ChunJun 实现数据实时同步?

如何有效地从 Firebase 实时数据库中获取 7 天、30 天的摘要?