Skip to content

Instantly share code, notes, and snippets.

@y-polek
Last active November 25, 2021 01:22
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save y-polek/42afe8fdb1518db471fb27e646526a06 to your computer and use it in GitHub Desktop.
Save y-polek/42afe8fdb1518db471fb27e646526a06 to your computer and use it in GitHub Desktop.
'map' and 'combineLatest' transformations for LiveData
import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MediatorLiveData
import android.arch.lifecycle.MutableLiveData
fun <X, Y> LiveData<X>.map(func: (X?) -> Y?): MutableLiveData<Y?> {
return MediatorLiveData<Y>().apply {
addSource(this@map) { x -> value = func(x) }
}
}
fun <X, Y> LiveData<X>.mapSkipNulls(func: (X) -> Y): MutableLiveData<Y> {
return MediatorLiveData<Y>().apply {
addSource(this@mapSkipNulls) { x ->
x ?: return@addSource
value = func(x)
}
}
}
fun <A, B> LiveData<A>.combineLatest(b: LiveData<B>): LiveData<Pair<A, B>> {
return MediatorLiveData<Pair<A, B>>().apply {
var lastA: A? = null
var lastB: B? = null
addSource(this@combineLatest) {
if (it == null && value != null) value = null
lastA = it
if (lastA != null && lastB != null) value = lastA!! to lastB!!
}
addSource(b) {
if (it == null && value != null) value = null
lastB = it
if (lastA != null && lastB != null) value = lastA!! to lastB!!
}
}
}
fun <A, B, C> combineLatest(a: LiveData<A>, b: LiveData<B>, c: LiveData<C>): LiveData<Triple<A?, B?, C?>> {
fun Triple<A?, B?, C?>?.copyWithFirst(first: A?): Triple<A?, B?, C?> {
if (this@copyWithFirst == null) return Triple<A?, B?, C?>(first, null, null)
return this@copyWithFirst.copy(first = first)
}
fun Triple<A?, B?, C?>?.copyWithSecond(second: B?): Triple<A?, B?, C?> {
if (this@copyWithSecond == null) return Triple<A?, B?, C?>(null, second, null)
return this@copyWithSecond.copy(second = second)
}
fun Triple<A?, B?, C?>?.copyWithThird(third: C?): Triple<A?, B?, C?> {
if (this@copyWithThird == null) return Triple<A?, B?, C?>(null, null, third)
return this@copyWithThird.copy(third = third)
}
return MediatorLiveData<Triple<A?, B?, C?>>().apply {
addSource(a) { value = value.copyWithFirst(it) }
addSource(b) { value = value.copyWithSecond(it) }
addSource(c) { value = value.copyWithThird(it) }
}
}
import android.arch.core.executor.testing.InstantTaskExecutorRule
import android.arch.lifecycle.MutableLiveData
import android.arch.lifecycle.Observer
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
class LiveDataExtTest {
@Rule
@JvmField
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Test
fun testMap() {
val ints = MutableLiveData<Int>()
val squareInts = ints.map { n ->
n ?: return@map null
n * n
}
val observer = mock<Observer<Int?>>()
squareInts.observeForever(observer)
ints.value = 2
assertThat(squareInts.value).isEqualTo(4)
ints.value = null
assertThat(squareInts.value).isEqualTo(null)
ints.value = 3
assertThat(squareInts.value).isEqualTo(9)
ints.value = 4
assertThat(squareInts.value).isEqualTo(16)
verify(observer, times(4)).onChanged(any())
}
@Test
fun testMapSkipNulls() {
val ints = MutableLiveData<Int>()
val squareInts = ints.mapSkipNulls { n ->
n * n
}
val observer = mock<Observer<Int?>>()
squareInts.observeForever(observer)
ints.value = 2
assertThat(squareInts.value).isEqualTo(4)
ints.value = null
assertThat(squareInts.value).isEqualTo(4)
ints.value = 3
assertThat(squareInts.value).isEqualTo(9)
ints.value = 4
assertThat(squareInts.value).isEqualTo(16)
verify(observer, times(3)).onChanged(any())
}
@Test
fun testCombineLatest() {
val ints = MutableLiveData<Int>()
val chars = MutableLiveData<Char>()
val pairs = ints.combineLatest(chars)
pairs.observeForever {}
ints.value = 1
assertThat(pairs.value).isNull()
chars.value = 'A'
assertThat(pairs.value).isEqualTo(1 to 'A')
ints.value = 2
assertThat(pairs.value).isEqualTo(2 to 'A')
chars.value = 'B'
assertThat(pairs.value).isEqualTo(2 to 'B')
}
@Test
fun testCombineLatestClearOnNull() {
val ints = MutableLiveData<Int>()
val chars = MutableLiveData<Char>()
val pairs = ints.combineLatest(chars)
pairs.observeForever {}
ints.value = 1
assertThat(pairs.value).isEqualTo(null)
chars.value = 'A'
assertThat(pairs.value).isEqualTo(1 to 'A')
ints.value = null
assertThat(pairs.value).isNull()
ints.value = 2
assertThat(pairs.value).isEqualTo(2 to 'A')
}
@Test
fun testCombineLatestCallCount() {
val ints = MutableLiveData<Int>()
val chars = MutableLiveData<Char>()
val pairs = ints.combineLatest(chars)
val observer = mock<Observer<Pair<Int, Char>>>()
pairs.observeForever(observer)
ints.value = 1
chars.value = 'A'
ints.value = null
chars.value = null
ints.value = 2
chars.value = 'B'
verify(observer, times(1)).onChanged(1 to 'A')
verify(observer, times(1)).onChanged(null)
verify(observer, times(1)).onChanged(2 to 'B')
}
@Test
fun testCombineLatestTriple() {
val hostLiveData = MutableLiveData<String>()
val userLiveData = MutableLiveData<String>()
val passwordLiveData = MutableLiveData<String>()
val credentialsLiveData = combineLatest(hostLiveData, userLiveData, passwordLiveData)
val observer = mock<Observer<Triple<String?, String?, String?>>>()
credentialsLiveData.observeForever(observer)
assertThat(credentialsLiveData.value).isNull()
hostLiveData.value = "Raspberry Pi"
assertThat(credentialsLiveData.value).isEqualTo(Triple("Raspberry Pi", null, null))
userLiveData.value = "admin"
assertThat(credentialsLiveData.value).isEqualTo(Triple("Raspberry Pi", "admin", null))
passwordLiveData.value = "12345"
assertThat(credentialsLiveData.value).isEqualTo(Triple("Raspberry Pi", "admin", "12345"))
hostLiveData.value = null
assertThat(credentialsLiveData.value).isEqualTo(Triple(null, "admin", "12345"))
userLiveData.value = null
assertThat(credentialsLiveData.value).isEqualTo(Triple(null, null, "12345"))
passwordLiveData.value = null
assertThat(credentialsLiveData.value).isEqualTo(Triple(null, null, null))
}
private inline fun <reified T: Any> mock(): T = Mockito.mock(T::class.java)
}
Copy link

ghost commented Oct 8, 2018

FYI you could delegate map to Transformations.map (https://developer.android.com/reference/android/arch/lifecycle/Transformations)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment