MediatorLiveData或使用多个参数的switchMap转换

51

我在ViewModel中使用Transformations.switchMap,以便我的fragment中观察到的LiveData集合能够对code参数的更改做出反应。

这个很完美:

public class MyViewModel extends AndroidViewModel {

    private final LiveData<DayPrices> dayPrices;
    private final MutableLiveData<String> code = new MutableLiveData<>();
    // private final MutableLiveData<Integer> nbDays = new MutableLiveData<>();
    private final DBManager dbManager;

    public MyViewModel(Application application) {
        super(application);
        dbManager = new DBManager(application.getApplicationContext());
        dayPrices = Transformations.switchMap(
            code,
            value -> dbManager.getDayPriceData(value/*, nbDays*/)
        );
    }

    public LiveData<DayPrices> getDayPrices() {
        return dayPrices;
    }

    public void setCode(String code) {
        this.code.setValue(code);
    }

    /*public void setNbDays(int nbDays) {
        this.nbDays.setValue(nbDays);
    }*/

}

public class MyFragment extends Fragment {

    private MyViewModel myViewModel;

    myViewModel = ViewModelProviders.of(this).get(MyViewModel.class);
    myViewModel.setCode("SO");
    //myViewModel.setNbDays(30);
    myViewModel.getDayPrices().observe(MyFragment.this, dataList -> {
        // update UI with data from dataList
    });
}

问题
现在我需要另一个参数(上面代码中已注释的nbDays),以便我的LiveData对象对两个参数的更改(codenbDays)做出反应。
如何链接转换?
一些阅读指向MediatorLiveData,但它并没有解决我的问题(仍然需要调用带有2个参数的单个DB函数,我不需要合并2个liveDatas)。
所以我尝试了这个而不是switchMap,但是codenbDays始终为null。
dayPrices.addSource(
    dbManager.getDayPriceData(code.getValue(), nbDays.getValue),
    apiResponse -> dayPrices.setValue(apiResponse)
);

一种解决方法是将对象作为单个参数传递,但我相信这个问题有一个简单的解决方案。

5个回答

70

来源:https://plus.google.com/+MichielPijnackerHordijk/posts/QGXF9gRomVi

如果要为switchMap()设置多个触发器,您需要使用自定义的MediatorLiveData来观察LiveData对象的组合 -

class CustomLiveData extends MediatorLiveData<Pair<String, Integer>> {
    public CustomLiveData(LiveData<String> code, LiveData<Integer> nbDays) {
        addSource(code, new Observer<String>() {
            public void onChanged(@Nullable String first) {
                setValue(Pair.create(first, nbDays.getValue()));
            }
        });
        addSource(nbDays, new Observer<Integer>() {
            public void onChanged(@Nullable Integer second) {
                setValue(Pair.create(code.getValue(), second));
            }
        });
    }
}

那么你可以这样做 -

CustomLiveData trigger = new CustomLiveData(code, nbDays);
LiveData<DayPrices> dayPrices = Transformations.switchMap(trigger, 
    value -> dbManager.getDayPriceData(value.first, value.second));

如果您使用Kotlin并且想要使用泛型:

class DoubleTrigger<A, B>(a: LiveData<A>, b: LiveData<B>) : MediatorLiveData<Pair<A?, B?>>() {
    init {
        addSource(a) { value = it to b.value }
        addSource(b) { value = a.value to it }
    }
}

然后:

val dayPrices = Transformations.switchMap(DoubleTrigger(code, nbDays)) {
    dbManager.getDayPriceData(it.first, it.second)
}

谢谢,非常有帮助!但在这种情况下,switchMap 连续初始化两次,是否可能跳过第一个源呢? - aleksandrbel
@aleksandrbel 你的意思是说,使用 codenbDays 的单独 setter 可以触发两次 switchMap 吗?那么,观察者可能会接收到两个连续的值。如果你不想要这种行为,最好在 MutableLiveData 中只使用一个 Pair 对象来组合这两个值,就像下面 OP 的答案中所示。 - jL4

24

根据 @jL4 提出的建议,使用自定义 MediatorLiveData 是非常好的解决方案。

我想分享最简单的解决方案,即使用内部类来表示组合过滤器值:

public class MyViewModel extends AndroidViewModel {

    private final LiveData<DayPrices> dayPrices;
    private final DBManager dbManager;
    private final MutableLiveData<DayPriceFilter> dayPriceFilter;

    public MyViewModel(Application application) {
        super(application);
        dbManager = new DBManager(application.getApplicationContext());
        dayPriceFilter = new MutableLiveData<>();
        dayPrices = Transformations.switchMap(dayPriceFilter, input -> dbManager.getDayPriceData(input.code, input.nbDays));
    }

    public LiveData<DayPrices> getDayPrices() {
        return dayPrices;
    }

    public void setDayPriceFilter(String code, int nbDays) {
        DayPriceFilter update = new DayPriceFilter(code, nbDays);
        if (Objects.equals(dayPriceFilter.getValue(), update)) {
            return;
        }
        dayPriceFilter.setValue(update);
    }

    static class DayPriceFilter {
        final String code;
        final int nbDays;

        DayPriceFilter(String code, int nbDays) {
            this.code = code == null ? null : code.trim();
            this.nbDays = nbDays;
        }
    }

}

然后在活动/片段中:

public class MyFragment extends Fragment {

    private MyViewModel myViewModel;

    myViewModel = ViewModelProviders.of(this).get(MyViewModel.class);
    myViewModel.setDayPriceFilter("SO", 365);
    myViewModel.getDayPrices().observe(MyFragment.this, dataList -> {
        // update UI with data from dataList
    });
}

1
你可能想在DayPriceFilter中实现“equals”。 - aeracode

22

对jL4答案的简化,(也为了帮助任何人)也使用Kotlin ... 不需要为此创建自定义类:

class YourViewModel: ViewModel() {

    val firstLiveData: LiveData<String> // or whatever type
    val secondLiveData: LiveData<Int> // or whatever

    // the Pair values are nullable as getting "liveData.value" can be null
    val combinedValues = MediatorLiveData<Pair<String?, Int?>>().apply {
        addSource(firstLiveData) { 
           value = Pair(it, secondLiveData.value)
        }
        addSource(secondLiveData) { 
           value = Pair(firstLiveData.value, it)
        }
    }

    val results = Transformations.switchMap(combinedValues) { pair ->
      val firstValue = pair.first
      val secondValue = pair.second
      if (firstValue != null && secondValue != null) {
         yourDataSource.yourLiveDataCall(firstValue, secondValue)
      } else null
    }

}

解释

firstLiveDatasecondLiveData 的任何更新都将更新 combinedValues 的值,并将这两个值作为一对发出(感谢 jL4)。

调用 liveData.value 可能为 null,因此该解决方案使 Pair 中的值可为空以避免空指针异常。

因此,在实际的结果/数据源调用中,switch map 在 combinedValues live data 上进行,然后从 Pair 中提取 2 个值并执行空检查,以确保向数据源传递非 null 值。


这是一个非常好的解决方案。有没有办法用N个来源来扩展它?我有一个情况,来源数量是未知的..! - james04

1
我使用以下类来转换许多不同类型的实时数据。
class MultiMapLiveData<T>(
    private val liveDataSources: Array<LiveData<*>>,
    private val waitFirstValues: Boolean = true,
    private val transform: (signalledLiveData: LiveData<*>) -> T
): LiveData<T>() {
    private val mObservers = ArrayList<Observer<Any>>()
    private var mInitializedSources = mutableSetOf<LiveData<*>>()

    override fun onActive() {
        super.onActive()

        if (mObservers.isNotEmpty()) throw InternalError(REACTIVATION_ERROR_MESSAGE)
        if (mInitializedSources.isNotEmpty()) throw InternalError(REACTIVATION_ERROR_MESSAGE)

        for (t in liveDataSources.indices) {
            val liveDataSource = liveDataSources[t]
            val observer = Observer<Any> {
                if (waitFirstValues) {
                    if (mInitializedSources.size < liveDataSources.size) {
                        mInitializedSources.add(liveDataSource)
                    }
                    if (mInitializedSources.size == liveDataSources.size) {
                        value = transform(liveDataSource)
                    }
                } else {
                    value = transform(liveDataSource)
                }
            }
            liveDataSource.observeForever(observer)
            mObservers.add(observer)
        }
    }

    override fun onInactive() {
        super.onInactive()
        for (t in liveDataSources.indices) {
            val liveDataSource = liveDataSources[t]
            val observer = mObservers[t]
            liveDataSource.removeObserver(observer)
        }
        mObservers.clear()
        mInitializedSources.clear()
    }

    companion object {
        private const val REACTIVATION_ERROR_MESSAGE = "Reactivation of active LiveData"
    }
}


class MyTransformations {
    companion object {
        fun <T> multiMap(
            liveDataSources: Array<LiveData<*>>,
            waitFirstValues: Boolean = true,
            transform: (signalledLiveData: LiveData<*>) -> T
        ): LiveData<T> {
            return MultiMapLiveData(liveDataSources, waitFirstValues, transform)
        }

        fun <T> multiSwitch(
            liveDataSources: Array<LiveData<*>>,
            waitFirstValues: Boolean = true,
            transform: (signalledLiveData: LiveData<*>) -> LiveData<T>
        ): LiveData<T> {
            return Transformations.switchMap(
                multiMap(liveDataSources, waitFirstValues) {
                    transform(it)
                }) {
                    it
                }
        }
    }
}

用法: 需要注意的是工作逻辑略有不同。导致更新的LiveData(signalledLiveData)作为参数传递给转换监听器,而不是所有LiveData的值。您可以通过value属性以通常的方式自己获取当前的LiveData值。

示例:

class SequenceLiveData(
    scope: CoroutineScope,
    start: Int,
    step: Int,
    times: Int
): LiveData<Int>(start) {
    private var current = start
    init {
        scope.launch {
            repeat (times) {
                value = current
                current += step
                delay(1000)
            }
        }
    }
}



suspend fun testMultiMap(lifecycleOwner: LifecycleOwner, scope: CoroutineScope) {
    val liveS = MutableLiveData<String>("aaa")
    val liveI = MutableLiveData<Int>()
    val liveB = MutableLiveData<Boolean>()

    val multiLiveWait: LiveData<String> = MyTransformations.multiMap(arrayOf(liveS, liveI, liveB)) {
        when (it) {
            liveS -> log("liveS changed")
            liveI -> log("liveI changed")
            liveB -> log("liveB changed")
        }
        "multiLiveWait: S = ${liveS.value}, I = ${liveI.value}, B = ${liveB.value}"
    }

    val multiLiveNoWait: LiveData<String> = MyTransformations.multiMap(arrayOf(liveS, liveI, liveB), false) {
        when (it) {
            liveS -> log("liveS changed")
            liveI -> log("liveI changed")
            liveB -> log("liveB changed")
        }
        "multiLiveNoWait: S = ${liveS.value}, I = ${liveI.value}, B = ${liveB.value}"
    }

    multiLiveWait.observe(lifecycleOwner) {
        log(it)
    }

    multiLiveNoWait.observe(lifecycleOwner) {
        log(it)
    }

    scope.launch {
        delay(1000)
        liveS.value = "bbb"
        delay(1000)
        liveI.value = 2222
        delay(1000)
        liveB.value = true          // ***
        delay(1000)
        liveI.value = 3333


        //  multiLiveWait generates:
        //
        //           <-- waits until all sources get first values (***)
        //
        //      liveB changed: S = bbb, I = 2222, B = true
        //      liveI changed: S = bbb, I = 3333, B = true

        //  multiLiveNoWait generates:
        //      liveS changed: S = aaa, I = null, B = null
        //      liveS changed: S = bbb, I = null, B = null
        //      liveI changed: S = bbb, I = 2222, B = null
        //      liveB changed: S = bbb, I = 2222, B = true      <-- ***
        //      liveI changed: S = bbb, I = 3333, B = true

    }
}

suspend fun testMultiMapSwitch(lifecycleOwner: LifecycleOwner, scope: CoroutineScope) {
    scope.launch {
        val start1 = MutableLiveData(0)
        val step1 = MutableLiveData(1)
        val multiLiveData = MyTransformations.multiSwitch(arrayOf(start1, step1)) {
            SequenceLiveData(scope, start1.value!!, step1.value!!, 5)
        }

        multiLiveData.observe(lifecycleOwner) {
            log("$it")
        }
        delay(7000)

        start1.value = 100
        step1.value = 2
        delay(7000)

        start1.value = 200
        step1.value = 3
        delay(7000)


        // generates:
        //      0
        //      1
        //      2
        //      3
        //      4
        //      100     <-- start.value = 100
        //      100     <-- step.value = 2
        //      102
        //      104
        //      106
        //      108
        //      200     <-- start.value = 200
        //      200     <-- step.value = 3
        //      203
        //      206
        //      209
        //      212

    }
}

0

我曾经遇到过类似的问题。有两种方法可以解决:

  1. 使用 MediatorLiveData
  2. 使用 RxJava,因为它有各种操作符来处理这种复杂的事情

如果你不了解 RxJava,那么我建议你编写自己的自定义 MediatorLiveData 类。 要学习如何编写自定义 MediatorLiveData 类,请查看此示例: https://gist.github.com/AkshayChordiya/a79bfcc422fd27d52b15cdafc55eac6b


我该如何进行需要4个LiveData(例如userId、email、apiKey等)的网络调用?Transformation.switchMap只接受一个LiveData参数。请给予一些建议。谢谢。 - amlwin
我遇到了类似的情况,我使用了MediatorLiveData。我建议你也试试看,看它是否适合你的用例,否则你可以使用RxJava,然后将其转换为LiveData - Akshay Chordiya
如果您不介意的话,能否分享一些代码片段给我? - amlwin
很遗憾,这个链接已经失效了。 - Richard Le Mesurier
1
@RichardLeMesurier 因为存储库不断变化,所以它一直变得无效。因此,现在我已经添加了一个链接到我的gist,这样就不会出问题了。 - Akshay Chordiya
1
令人沮丧,是的。我也刚刚发布了一个图像链接作为答案,我知道它很快也会失效... - Richard Le Mesurier

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