如何将两个实时数据依次合并?

59

我有以下使用案例:用户进入注册表单,输入姓名、电子邮件和密码,然后点击注册按钮。之后系统需要检查电子邮件是否被占用,根据结果显示错误信息或创建新用户...

我正在尝试使用Room、ViewModel和LiveData来完成这个任务。这是一个我尝试学习这些组件的项目,我没有远程API,我将把所有内容存储在本地数据库中。

因此,我有以下类:

  • RegisterActivity
  • RegisterViewModel
  • User
  • UsersDAO
  • UsersRepository
  • 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...


1
所以 performRegistration 基本上是一个插入方法?而且,并不是所有的 Dao 方法都应该返回 LiveData - Suleyman
1
是的,但它需要检查该电子邮件是否已被使用。 - clzola
1
所以在插入之前,您想查询数据库以检查电子邮件是否已经存在,对吗? - Suleyman
1
是的,但DAO.getUserByEmail()返回LiveData。 - clzola
1
你应该查看架构组件的指南 https://developer.android.com/jetpack/docs/guide。在UsersRegistrationService类中,你需要一个MediatorLivedata,其中你将为每个用户注册状态添加LiveDatas作为源。 - user
显示剩余4条评论
13个回答

119
你可以使用我的辅助方法:
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
}

22
如此优雅简洁,听起来有些邪恶 :) - Fabio
6
美丽的... :) - A1m
1
我已经尝试过了,它不会等待LiveData都准备好,即A为null,B已准备好,它仍然会触发观察者...有没有办法等待两个都准备好? - Amos
@Amos 试试这个 https://dev59.com/e1UL5IYBdhLWcg3wQWOD#70901284 - M-Wajeeh
谢谢@M-WaJeEh,我发现.combine()函数可行。 - Amos

36

借助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

5
为了使这段代码能够编译通过(至少在 Android Studio 3.4 中),我不得不在 addSource() 方法的签名中,在 T 类型参数前添加 "in"。override fun <T : Any?> addSource(source: LiveData<T>, onChanged: Observer<in T>) { - Brian Stewart
2
如果源计数是不固定的而不是两个呢? - Saman Sattari
3
我该如何在Java中实现“合并”部分? - adriennoir
2
@EpicPandaForce,你不需要覆盖,但它们只是为了防止误用。 - Joe Maher
1
为什么要重写addSourceremoveSource,使这个对象有效地成为LiveData,但仍然实现MediatorLiveData?这不会防止误用,反而会使应用程序变得脆弱。 - ruX
显示剩余5条评论

12

一种方法是使用流来实现这个目的。

val profile = MutableLiveData<ProfileData>()
val user = MutableLiveData<CurrentUser>()

val titleFlow = profile.asFlow().combine(user.asFlow()){ profile, user ->
    "${profile.job} ${user.name}"
}

然后是你的Fragment/Activity:

viewLifecycleOwner.lifecycleScope.launch { 
    viewModel.titleFlow.collectLatest { title ->
        Log.d(">>", title)
    }
}

这种方法的优点是titleFlow仅在两个LiveData至少发射了一个值时才会发出值。这个交互式图表将帮助您理解:https://rxmarbles.com/#combineLatest

另一种语法:

val titleFlow = combine(profile.asFlow(), user.asFlow()){ profile, user ->
    "${profile.job} ${user.name}"
}

我已经尝试过这个,但是得到了两个titleFlow的发射。第一次更新一个实时数据,第二次更新两个实时数据。也就是说,在发射之前它不会等待两者都完成。在我的ViewModel中:private val _liveDataA = MutableLiveData(10), private val _liveDataB = MutableLiveData(20) val combinedLiveData = combine(_liveDataA.asFlow(), _liveDataB.asFlow()) { a, b ->"$a $b"}。在我的lifecycleOwner中:var combinedLiveData = lifecycleOwner.lifecycleScope.launch{viewModel.combinedLiveData.collectLatest { combined -> Log.d(">>", combined)}} - mars8
我已经创建了这篇帖子 https://stackoverflow.com/q/71983906/15597975 - mars8

9

没有自定义类

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

9

Jose Alcérreca可能给出了关于这个问题的最佳答案:

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
}

11
请注意,这不会为您合并结果。它只会在任一来源更改时通知您。 - TheRealChx101
3
@TheRealChx101,合并数据的方式在链接的文章中有详细说明,基本上是使用combineLatestData函数。 - Stachu

5
您可以定义一个方法,使用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<A,B,C>,并暴露一个Triple<A,B,C>而不是Pair等。就像在https://dev59.com/m7Hma4cB1Zd3GeqPIT37#54292960中所示。
编辑:嘿,看,我甚至为你做了一个库,它可以从2个元素到16个元素进行操作:https://github.com/Zhuinden/livedata-combinetuple-kt

1
我们能不能用更通用的方式呢?例如,对于4个实时数据,我们需要添加另一个类,以此类推... - Saman Sattari
1
通常情况下,如果您需要同时组合超过6个类,我会感到惊讶。因此,拥有6个类(加上元组)并不太糟糕。 - EpicPandaForce
1
我来自未来,我有一个可以合并11元组的方法,谁知道呢? - EpicPandaForce
1
我们如何在片段中实际使用这个类? - AndroidDev123
2
@AndroidDev123,你不需要使用这个类,可以使用编辑中的库:https://github.com/Zhuinden/livedata-combinetuple-kt 现在你可以说combineTuple(liveData1, liveData2),它会正常工作。这是一个Kotlin库。 - EpicPandaForce
显示剩余4条评论

5

我基于@guness的回答进行了一种方法。我发现仅限于两个LiveData是不太好的。如果我们需要使用3个,怎么办?我们需要为每种情况创建不同的类。因此,我创建了一个处理无限量LiveData的类。

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

我在 value = combine(datas) 处遇到了 类型不匹配错误,要求是 R?,但实际上是 Unit - imin
这是因为你的 combine 方法返回的是 Unit 而不是 R。请注意类文档中的 return a SomeType value - Damia Fuentes
感谢您。这是一个有用的辅助类,但需要注意的是,combine函数的返回类型应与CombinedLiveData类中声明的SomeType的<R>泛型类型相同。例如,如果SomeType是Boolean?(可空值),则combine方法也必须返回Boolean?类型。 - Reyhane Farshbaf
一定要观察返回的mediatorLiveData,否则它将不会推送更新。 - Nilesh Deokar

3
许多答案都是有效的,但也假定LiveData泛型类型不可为空。
但如果一个或多个给定的输入类型是可空类型(考虑到Kotlin默认的上限为Any?,它是可空的)呢? 结果会是LiveData发射器将发出一个值(null),但MediatorLiveData将忽略它,认为它自己的子LiveData值没有被设置。
相反,这个解决方案通过强制要求传递给中介者的类型上限不能为空来解决这个问题。简单而必需的。
此外,这个实现避免了在合并函数调用后出现相同的值,这可能是你需要的,也可能不是,所以请随意删除那里的相等检查。
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()
    }
}

2
LiveData liveData1 = ...;
 LiveData liveData2 = ...;

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

6
复制粘贴得很好,但解释会更好。 - Farid

2

如果你想要两个值都不为空

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
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接