From 0b53c7ed001e4033eb6b9b8f64019c42ee5a9e13 Mon Sep 17 00:00:00 2001 From: Sven Obser Date: Mon, 24 Jun 2019 22:14:23 +0200 Subject: [PATCH] Closes adibfara/Lives#20: Zip and CombineLatest crash when null values are posted --- .../livedataextensions/Combining.kt | 251 ++++++++++-------- .../livedataextensions/CombiningTest.kt | 109 +++++++- 2 files changed, 248 insertions(+), 112 deletions(-) diff --git a/lives/src/main/java/com/snakydesign/livedataextensions/Combining.kt b/lives/src/main/java/com/snakydesign/livedataextensions/Combining.kt index 1870e36..1c06822 100644 --- a/lives/src/main/java/com/snakydesign/livedataextensions/Combining.kt +++ b/lives/src/main/java/com/snakydesign/livedataextensions/Combining.kt @@ -23,7 +23,6 @@ fun LiveData.mergeWith(vararg liveDatas: LiveData): LiveData { return merge(mergeWithArray) } - /** * Merges multiple LiveData, and emits any item that was emitted by any of them */ @@ -56,178 +55,199 @@ fun LiveData.startWith(startingValue: T?): LiveData { /** * zips both of the LiveData and emits a value after both of them have emitted their values, - * after that, emits values whenever any of them emits a value. + * after that, emits values whenever both of them emit another value. * * The difference between combineLatest and zip is that the zip only emits after all LiveData * objects have a new value, but combineLatest will emit after any of them has a new value. */ -fun zip(first: LiveData, second: LiveData): LiveData> { - return zip(first, second) { t, y -> Pair(t, y) } +fun zip(first: LiveData, second: LiveData): LiveData> { + return zip(first, second) { x, y -> Pair(x, y) } } -fun zip(first: LiveData, second: LiveData, zipFunction: (T, Y) -> Z): LiveData { - val finalLiveData: MediatorLiveData = MediatorLiveData() +/** + * zips both of the LiveData and emits a value after both of them have emitted their values, + * after that, emits values whenever both of them emit another value. + * + * The difference between combineLatest and zip is that the zip only emits after all LiveData + * objects have a new value, but combineLatest will emit after any of them has a new value. + */ +fun zip(first: LiveData, second: LiveData, zipFunction: (X?, Y?) -> R): LiveData { + val finalLiveData: MediatorLiveData = MediatorLiveData() + + val firstEmit: Emit = Emit() + val secondEmit: Emit = Emit() - var firstEmitted = false - var firstValue: T? = null + val combine: () -> Unit = { + if (firstEmit.emitted && secondEmit.emitted) { + val combined = zipFunction(firstEmit.value, secondEmit.value) + firstEmit.reset() + secondEmit.reset() + finalLiveData.value = combined + } + } - var secondEmitted = false - var secondValue: Y? = null finalLiveData.addSource(first) { value -> - firstEmitted = true - firstValue = value - if (firstEmitted && secondEmitted) { - finalLiveData.value = zipFunction(firstValue!!, secondValue!!) - firstEmitted = false - secondEmitted = false - } + firstEmit.value = value + combine() } finalLiveData.addSource(second) { value -> - secondEmitted = true - secondValue = value - if (firstEmitted && secondEmitted) { - finalLiveData.value = zipFunction(firstValue!!, secondValue!!) - firstEmitted = false - secondEmitted = false - } + secondEmit.value = value + combine() } return finalLiveData } - /** * zips three LiveData and emits a value after all of them have emitted their values, - * after that, emits values whenever any of them emits a value. + * after that, emits values whenever all of them emit another value. * * The difference between combineLatest and zip is that the zip only emits after all LiveData * objects have a new value, but combineLatest will emit after any of them has a new value. */ -fun zip(first: LiveData, second: LiveData, third: LiveData, zipFunction: (T, Y, X) -> Z): LiveData { - val finalLiveData: MediatorLiveData = MediatorLiveData() - - var firstEmitted = false - var firstValue: T? = null +fun zip( + first: LiveData, + second: LiveData, + third: LiveData, + zipFunction: (X?, Y?, Z?) -> R +): LiveData { + val finalLiveData: MediatorLiveData = MediatorLiveData() - var secondEmitted = false - var secondValue: Y? = null + val firstEmit: Emit = Emit() + val secondEmit: Emit = Emit() + val thirdEmit: Emit = Emit() - var thirdEmitted = false - var thirdValue: X? = null - finalLiveData.addSource(first) { value -> - firstEmitted = true - firstValue = value - if (firstEmitted && secondEmitted && thirdEmitted) { - finalLiveData.value = zipFunction(firstValue!!, secondValue!!, thirdValue!!) - firstEmitted = false - secondEmitted = false - thirdEmitted = false + val combine: () -> Unit = { + if (firstEmit.emitted && secondEmit.emitted && thirdEmit.emitted) { + val combined = zipFunction(firstEmit.value, secondEmit.value, thirdEmit.value) + firstEmit.reset() + secondEmit.reset() + thirdEmit.reset() + finalLiveData.value = combined } } + finalLiveData.addSource(first) { value -> + firstEmit.value = value + combine() + } finalLiveData.addSource(second) { value -> - secondEmitted = true - secondValue = value - if (firstEmitted && secondEmitted && thirdEmitted) { - firstEmitted = false - secondEmitted = false - thirdEmitted = false - finalLiveData.value = zipFunction(firstValue!!, secondValue!!, thirdValue!!) - } + secondEmit.value = value + combine() } - finalLiveData.addSource(third) { value -> - thirdEmitted = true - thirdValue = value - if (firstEmitted && secondEmitted && thirdEmitted) { - firstEmitted = false - secondEmitted = false - thirdEmitted = false - finalLiveData.value = zipFunction(firstValue!!, secondValue!!, thirdValue!!) - } + thirdEmit.value = value + combine() } - return finalLiveData } -fun zip(first: LiveData, second: LiveData, third: LiveData): LiveData> { - return zip(first, second, third) { t, y, x -> Triple(t, y, x) } +/** + * zips three LiveData and emits a value after all of them have emitted their values, + * after that, emits values whenever all of them emit another value. + * + * The difference between combineLatest and zip is that the zip only emits after all LiveData + * objects have a new value, but combineLatest will emit after any of them has a new value. + */ +fun zip(first: LiveData, second: LiveData, third: LiveData): LiveData> { + return zip(first, second, third) { x, y, z -> Triple(x, y, z) } } /** - * Combines the latest values from two LiveData objects. - * First emits after both LiveData objects have emitted a value, and will emit afterwards after any + * Combines the latest values from multiple LiveData objects. + * First emits after all LiveData objects have emitted a value, and will emit afterwards after any * of them emits a new value. * * The difference between combineLatest and zip is that the zip only emits after all LiveData * objects have a new value, but combineLatest will emit after any of them has a new value. */ -fun combineLatest(first: LiveData, second: LiveData, combineFunction: (X, T) -> Z): LiveData { - val finalLiveData: MediatorLiveData = MediatorLiveData() +fun combineLatest(first: LiveData, second: LiveData, combineFunction: (X?, Y?) -> R): LiveData { + val finalLiveData: MediatorLiveData = MediatorLiveData() - var firstEmitted = false - var firstValue: X? = null + val firstEmit: Emit = Emit() + val secondEmit: Emit = Emit() + + val combine: () -> Unit = { + if (firstEmit.emitted && secondEmit.emitted) { + val combined = combineFunction(firstEmit.value, secondEmit.value) + finalLiveData.value = combined + } + } - var secondEmitted = false - var secondValue: T? = null finalLiveData.addSource(first) { value -> - firstEmitted = true - firstValue = value - if (firstEmitted && secondEmitted) { - finalLiveData.value = combineFunction(firstValue!!, secondValue!!) - } + firstEmit.value = value + combine() } finalLiveData.addSource(second) { value -> - secondEmitted = true - secondValue = value - if (firstEmitted && secondEmitted) { - finalLiveData.value = combineFunction(firstValue!!, secondValue!!) - } + secondEmit.value = value + combine() } return finalLiveData } + +/** + * Combines the latest values from multiple LiveData objects. + * First emits after all LiveData objects have emitted a value, and will emit afterwards after any + * of them emits a new value. + * + * The difference between combineLatest and zip is that the zip only emits after all LiveData + * objects have a new value, but combineLatest will emit after any of them has a new value. + */ +fun combineLatest(first: LiveData, second: LiveData): LiveData> = + combineLatest(first, second) { x, y -> Pair(x, y) } + /** - * Combines the latest values from two LiveData objects. - * First emits after both LiveData objects have emitted a value, and will emit afterwards after any + * Combines the latest values from multiple LiveData objects. + * First emits after all LiveData objects have emitted a value, and will emit afterwards after any * of them emits a new value. * * The difference between combineLatest and zip is that the zip only emits after all LiveData * objects have a new value, but combineLatest will emit after any of them has a new value. */ -fun combineLatest(first: LiveData, second: LiveData, third: LiveData, combineFunction: (X, Y, T) -> Z): LiveData { - val finalLiveData: MediatorLiveData = MediatorLiveData() +fun combineLatest( + first: LiveData, + second: LiveData, + third: LiveData, + combineFunction: (X?, Y?, Z?) -> R +): LiveData { + val finalLiveData: MediatorLiveData = MediatorLiveData() - var firstEmitted = false - var firstValue: X? = null + val firstEmit: Emit = Emit() + val secondEmit: Emit = Emit() + val thirdEmit: Emit = Emit() - var secondEmitted = false - var secondValue: Y? = null + val combine: () -> Unit = { + if (firstEmit.emitted && secondEmit.emitted && thirdEmit.emitted) { + val combined = combineFunction(firstEmit.value, secondEmit.value, thirdEmit.value) + finalLiveData.value = combined + } + } - var thirdEmitted = false - var thirdValue: T? = null finalLiveData.addSource(first) { value -> - firstEmitted = true - firstValue = value - if (firstEmitted && secondEmitted && thirdEmitted) { - finalLiveData.value = combineFunction(firstValue!!, secondValue!!, thirdValue!!) - } + firstEmit.value = value + combine() } finalLiveData.addSource(second) { value -> - secondEmitted = true - secondValue = value - if (firstEmitted && secondEmitted && thirdEmitted) { - finalLiveData.value = combineFunction(firstValue!!, secondValue!!, thirdValue!!) - } + secondEmit.value = value + combine() } finalLiveData.addSource(third) { value -> - thirdEmitted = true - thirdValue = value - if (firstEmitted && secondEmitted && thirdEmitted) { - finalLiveData.value = combineFunction(firstValue!!, secondValue!!, thirdValue!!) - } + thirdEmit.value = value + combine() } return finalLiveData } +/** + * Combines the latest values from multiple LiveData objects. + * First emits after all LiveData objects have emitted a value, and will emit afterwards after any + * of them emits a new value. + * + * The difference between combineLatest and zip is that the zip only emits after all LiveData + * objects have a new value, but combineLatest will emit after any of them has a new value. + */ +fun combineLatest(first: LiveData, second: LiveData, third: LiveData): LiveData> = + combineLatest(first, second, third) { x, y, z -> Triple(x, y, z) } + /** * Converts the LiveData to `SingleLiveData` and concats it with the `otherLiveData` and emits their * values one by one @@ -280,4 +300,23 @@ fun LiveData.sampleWith(other: LiveData<*>): LiveData { } } return finalLiveData +} + +/** + * Wrapper that wraps an emitted value. + */ +private class Emit { + + internal var emitted: Boolean = false + + internal var value: T? = null + set(value) { + field = value + emitted = true + } + + fun reset() { + value = null + emitted = false + } } \ No newline at end of file diff --git a/lives/src/test/java/com/snakydesign/livedataextensions/CombiningTest.kt b/lives/src/test/java/com/snakydesign/livedataextensions/CombiningTest.kt index e6737af..7641432 100644 --- a/lives/src/test/java/com/snakydesign/livedataextensions/CombiningTest.kt +++ b/lives/src/test/java/com/snakydesign/livedataextensions/CombiningTest.kt @@ -3,10 +3,17 @@ package com.snakydesign.livedataextensions import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer -import org.junit.* +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers.any import org.mockito.Mockito +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.verifyZeroInteractions /** * Created by Adib Faramarzi @@ -92,7 +99,7 @@ class CombiningTest { @Test fun `test LiveData zip with another LiveData`() { - val observer = Mockito.mock(Observer::class.java) as Observer> + val observer = Mockito.mock(Observer::class.java) as Observer> val sourceLiveData1 = MutableLiveData() val sourceLiveData2 = MutableLiveData() val expectedResult = Pair(true, 3) @@ -112,7 +119,7 @@ class CombiningTest { @Test fun `test LiveData zip with another LiveData for second emission`() { - val observer = Mockito.mock(Observer::class.java) as Observer> + val observer = Mockito.mock(Observer::class.java) as Observer> val sourceLiveData1 = MutableLiveData() val sourceLiveData2 = MutableLiveData() val expectedResult = Pair(true, 3) @@ -135,7 +142,7 @@ class CombiningTest { @Test fun `test LiveData zip with 2 other LiveData`() { - val observer = Mockito.mock(Observer::class.java) as Observer> + val observer = Mockito.mock(Observer::class.java) as Observer> val sourceLiveData1 = MutableLiveData() val sourceLiveData2 = MutableLiveData() val sourceLiveData3 = MutableLiveData() @@ -155,9 +162,42 @@ class CombiningTest { verifyNoMoreInteractions(observer) } + @Test + fun `test LiveData zip with null values`() { + val observer = Mockito.mock(Observer::class.java) as Observer> + val sourceLiveData1 = MutableLiveData() + val sourceLiveData2 = MutableLiveData() + val sourceLiveData3 = MutableLiveData() + val testingLiveData = zip(sourceLiveData1, sourceLiveData2, sourceLiveData3) + testingLiveData.observeForever(observer) + + // Ensure there is no emit until all sources have emitted + sourceLiveData1.value = null + sourceLiveData2.value = null + Assert.assertEquals(null, testingLiveData.value) + verify(observer, never()).onChanged(any()) + + // After all emitted null, we expect an emit with null values + sourceLiveData3.value = null + val expectedResult = Triple(null, null, null) + Assert.assertEquals(expectedResult, testingLiveData.value) + verify(observer).onChanged(expectedResult) + + // Ensure there is no emit until all sources have emitted a new value + sourceLiveData2.value = 42 + sourceLiveData3.value = "42" + verifyZeroInteractions(observer) + + // After all emitted new value, we expect another emit + sourceLiveData1.value = true + val expectedResult2 = Triple(true, 42, "42") + Assert.assertEquals(expectedResult2, testingLiveData.value) + verify(observer).onChanged(expectedResult2) + } + @Test fun `test LiveData sample with another LiveData`() { - val observer = Mockito.mock(Observer::class.java) as Observer> + val observer = Mockito.mock(Observer::class.java) as Observer> val sourceLiveData1 = MutableLiveData() val sourceLiveData2 = MutableLiveData() val sourceLiveData3 = MutableLiveData() @@ -179,7 +219,7 @@ class CombiningTest { @Test fun `test LiveData combineLatest with another LiveData`() { - val observer = Mockito.mock(Observer::class.java) as Observer> + val observer = Mockito.mock(Observer::class.java) as Observer> val sourceLiveData1 = MutableLiveData() val sourceLiveData2 = MutableLiveData() val expectedResult = Pair(true, 3) @@ -209,6 +249,63 @@ class CombiningTest { verifyNoMoreInteractions(observer) } + @Test + fun `test LiveData combineLatest with null values`() { + val observer = Mockito.mock(Observer::class.java) as Observer> + val sourceLiveData1 = MutableLiveData() + val sourceLiveData2 = MutableLiveData() + val testingLiveData = combineLatest(sourceLiveData1, sourceLiveData2) { boolean, int -> Pair(boolean, int) } + testingLiveData.observeForever(observer) + + // Ensure there is no emit until all sources have emitted + sourceLiveData1.value = null + Assert.assertEquals(null, testingLiveData.value) + verify(observer, never()).onChanged(any()) + + // After all emitted null, we expect an emit with null values + sourceLiveData2.value = null + val expectedResult = Pair(null, null) + Assert.assertEquals(expectedResult, testingLiveData.value) + verify(observer).onChanged(expectedResult) + + // One emitted a non-null value + sourceLiveData2.value = 4 + val expectedResult2 = Pair(null, 4) + Assert.assertEquals(expectedResult2, testingLiveData.value) + verify(observer).onChanged(expectedResult2) + + // Both emitted a non-null value + sourceLiveData1.value = false + val expectedResult3 = Pair(false, 4) + Assert.assertEquals(expectedResult3, testingLiveData.value) + verify(observer).onChanged(expectedResult3) + verifyNoMoreInteractions(observer) + } + + @Test + fun `test LiveData combineLatest three with null values`() { + val observer = Mockito.mock(Observer::class.java) as Observer> + val sourceLiveData1 = MutableLiveData() + val sourceLiveData2 = MutableLiveData() + val sourceLiveData3 = MutableLiveData() + val testingLiveData = combineLatest(sourceLiveData1, sourceLiveData2, sourceLiveData3) { boolean, int, long -> + Triple(boolean, int, long) + } + testingLiveData.observeForever(observer) + + // Ensure there is no emit until all sources have emitted + sourceLiveData1.value = null + sourceLiveData2.value = null + Assert.assertEquals(null, testingLiveData.value) + verify(observer, never()).onChanged(any()) + + // After all emitted null, we expect an emit with null values + sourceLiveData3.value = null + val expectedResult = Triple(null, null, null) + Assert.assertEquals(expectedResult, testingLiveData.value) + verify(observer).onChanged(expectedResult) + } + @Test fun `test LiveData sampleWith another live data`() { val observer = Mockito.mock(Observer::class.java) as Observer