17

I have a value in the UI that it's value depends on two LiveData objects. Imagine a shop where you need a subtotal = sum of all items price and a total = subtotal + shipment price. Using Transformations we can do the following for the subtotal LiveData object (as it only depends on itemsLiveData):

val itemsLiveData: LiveData<List<Items>> = ...
val subtotalLiveData = Transformations.map(itemsLiveData) { 
   items ->
       getSubtotalPrice(items)
}

In the case of the total it would be great to be able to do something like this:

val shipPriceLiveData: LiveData<Int> = ...
val totalLiveData = Transformations.map(itemsLiveData, shipPriceLiveData) { 
   items, price ->
       getSubtotalPrice(items) + price
}

But, unfortunately, that's not possible because we cannot put more than one argument in the map function. Anyone knows a good way of achieving this?

Damia Fuentes
  • 5,308
  • 6
  • 33
  • 65

4 Answers4

13

I come up with another solution.

class PairLiveData<A, B>(first: LiveData<A>, second: LiveData<B>) : MediatorLiveData<Pair<A?, B?>>() {
    init {
        addSource(first) { value = it to second.value }
        addSource(second) { value = first.value to it }
    }
}

class TripleLiveData<A, B, C>(first: LiveData<A>, second: LiveData<B>, third: LiveData<C>) : MediatorLiveData<Triple<A?, B?, C?>>() {
    init {
        addSource(first) { value = Triple(it, second.value, third.value) }
        addSource(second) { value = Triple(first.value, it, third.value) }
        addSource(third) { value = Triple(first.value, second.value, it) }
    }
}

fun <A, B> LiveData<A>.combine(other: LiveData<B>): PairLiveData<A, B> {
    return PairLiveData(this, other)
}

fun <A, B, C> LiveData<A>.combine(second: LiveData<B>, third: LiveData<C>): TripleLiveData<A, B, C> {
    return TripleLiveData(this, second, third)
}

Then, you can combine multiple source.

val totalLiveData = Transformations.map(itemsLiveData.combine(shipPriceLiveData)) {
    // Do your stuff
}

If you want to have 4 or more sources, you need to create you own data class because Kotlin only has Pair and Triple.

In my opinion, there is no reason to run with uiThread in Damia's solution.

Joshua
  • 5,901
  • 2
  • 32
  • 52
  • Can you please explain this part of your code? How can I write it in a different way? `init { addSource(first) { value = it to second.value } addSource(second) { value = first.value to it } }` – Erkan Sep 28 '21 at 20:29
12

UPDATE

Based on my previous answer, I created a generic way where we can add as many live datas as we want.

import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData

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

OLD

At the end I used MediatorLiveData to achieve the same objective.

fun mapBasketTotal(source1: LiveData<List<Item>>, source2: LiveData<ShipPrice>): LiveData<String> {
    val result = MediatorLiveData<String>()
    uiThread {
        var subtotal: Int = 0
        var shipPrice: Int = 0
        fun sumAndFormat(){ result.value = format(subtotal + shipPrice)}
        result.addSource(source1, { items ->
            if (items != null) {
                subtotal = getSubtotalPrice(items)
                sumAndFormat()
            }
        })
        result.addSource(source2, { price ->
            if (price != null) {
                shipPrice = price
                sumAndFormat()
            }
        })
    }
    return result
}
Damia Fuentes
  • 5,308
  • 6
  • 33
  • 65
12

You can use switchMap() for such case, because it returns LiveData object which can be Transformations.map()

In below code I am getting sum of final amount of two objects onwardSelectQuote and returnSelectQuote

finalAmount = Transformations.switchMap(onwardSelectQuote) { data1 ->
            Transformations.map(returnSelectQuote) { data2 -> ViewUtils.formatRupee((data1.finalAmount!!.toFloat() + data2.finalAmount!!.toFloat()).toString())
            }
        }
Shahbaz Hashmi
  • 2,631
  • 2
  • 26
  • 49
  • Unfortunately, this seems to stuff a LiveData inside another LiveData. – Brill Pappin Jun 29 '20 at 13:48
  • 1
    @BrillPappin fortunately, it works perfectly for observing two LiveData together. – Shahbaz Hashmi Jun 30 '20 at 08:10
  • @BrillPappin I think this is not a problem to the given scenario, switchMap works accordingly with his implementation: https://developer.android.com/reference/android/arch/lifecycle/Transformations#switchmap, I think this is the best and more simple solution to the answer. – Razec Luar Feb 24 '22 at 11:27
  • It may work, but I feel it's a bit sloppy and may make it hard to find a bug later. I personally wouldn't write the code that way. – Brill Pappin Feb 25 '22 at 14:47
  • @BrillPappin Then, can you show your solution about this problem? I want to learn – 4rigener Oct 16 '22 at 14:23
  • You want to use a MediatorLiveData, there are several articles about how to do that. A quick google search shows up this one, which at a glance appears to be fairly succinct: https://medium.com/codex/how-to-use-mediatorlivedata-with-multiple-livedata-types-a40e1a59e8cf – Brill Pappin Oct 20 '22 at 13:11
0

I use following classes to transform many live data with different types

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
                }
        }
    }
}

Usage: Note that the logic of the work is slightly different. The LiveData that caused the update (signalledLiveData) is passed to the Tranformation Listener as parameter, NOT the values of all LiveData. You get the current LiveData values yourself in the usual way via value property.

examples:

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

    }
}