简体   繁体   中英

LiveData Transformations.map() with multiple arguments

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?

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
}

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

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.

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

    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM