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

How to combine two live data one after the other?

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

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

所以我有这些 类:

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

class RegisterViewModel extends ViewModel {

    //...

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

    //...

}

所以这是基本的想法,我也想要 performRegistration 到 return 给我新创建的用户。

最困扰我的是我不知道如何在服务中实现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 中的方法应该 return LiveData 因为 UsersDAO 正在 returning 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() 函数以及如何将值传回视图模型,然后如何将 activity 从 RegisterActivity 更改为 MainActivity...

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

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
}

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

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
}

您可以定义一个使用 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 等。就像在 中一样。

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

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

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

我根据@guness 的回答做了一个方法。我发现仅限于两个 LiveData 并不好。如果我们想使用 3 怎么办?我们需要为每种情况创建不同的 classes。因此,我创建了一个 class 来处理无限量的 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)
             }
         }
     }
 }

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

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
}

如果要在构造时创建字段并设置(使用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
    }
}

没有自定义 class

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

其中许多答案都有效,但还假设 LiveData 泛型类型不可为 null。

但是,如果一个或多个给定的输入类型是可为空的类型(假设泛型的默认 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()
    }
}

一种方法是为此使用流程。

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 只会在两个实时数据都至少发出一个值时才发出值。此交互图将帮助您理解此 https://rxmarbles.com/#combineLatest

替代语法:

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

已通过 LiveData 扩展解决

fun <T, R> LiveData<T>.map(action: (t: T) -> R): LiveData<R> =
    Transformations.map(this, action)

fun <T1, T2, R> LiveData<T1>.combine(
    liveData: LiveData<T2>,
    action: (t1: T1?, t2: T2?) -> R
): LiveData<R> =
    MediatorLiveData<Pair<T1?, T2?>>().also { med ->
        med.addSource(this) { med.value = it to med.value?.second }
        med.addSource(liveData) { med.value = med.value?.first to it }
    }.map { action(it.first, it.second) }