Merge "Flow utility functions: pairwise + setChanges" into tm-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
88684d2dd3
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.systemui.util.kotlin
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.drop
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.flow.zip
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new [Flow] that combines the two most recent emissions from [this] using [transform].
|
||||||
|
* Note that the new Flow will not start emitting until it has received two emissions from the
|
||||||
|
* upstream Flow.
|
||||||
|
*
|
||||||
|
* Useful for code that needs to compare the current value to the previous value.
|
||||||
|
*/
|
||||||
|
fun <T, R> Flow<T>.pairwiseBy(transform: suspend (old: T, new: T) -> R): Flow<R> {
|
||||||
|
// same as current flow, but with the very first event skipped
|
||||||
|
val nextEvents = drop(1)
|
||||||
|
// zip current flow and nextEvents; transform will receive a pair of old and new value. This
|
||||||
|
// works because zip will suppress emissions until both flows have emitted something; since in
|
||||||
|
// this case both flows are emitting at the same rate, but the current flow just has one extra
|
||||||
|
// thing emitted at the start, the effect is that zip will cache the most recent value while
|
||||||
|
// waiting for the next emission from nextEvents.
|
||||||
|
return zip(nextEvents, transform)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new [Flow] that combines the two most recent emissions from [this] using [transform].
|
||||||
|
* [initialValue] will be used as the "old" value for the first emission.
|
||||||
|
*
|
||||||
|
* Useful for code that needs to compare the current value to the previous value.
|
||||||
|
*/
|
||||||
|
fun <T, R> Flow<T>.pairwiseBy(
|
||||||
|
initialValue: T,
|
||||||
|
transform: suspend (previousValue: T, newValue: T) -> R,
|
||||||
|
): Flow<R> =
|
||||||
|
onStart { emit(initialValue) }.pairwiseBy(transform)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new [Flow] that produces the two most recent emissions from [this]. Note that the new
|
||||||
|
* Flow will not start emitting until it has received two emissions from the upstream Flow.
|
||||||
|
*
|
||||||
|
* Useful for code that needs to compare the current value to the previous value.
|
||||||
|
*/
|
||||||
|
fun <T> Flow<T>.pairwise(): Flow<WithPrev<T>> = pairwiseBy(::WithPrev)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new [Flow] that produces the two most recent emissions from [this]. [initialValue]
|
||||||
|
* will be used as the "old" value for the first emission.
|
||||||
|
*
|
||||||
|
* Useful for code that needs to compare the current value to the previous value.
|
||||||
|
*/
|
||||||
|
fun <T> Flow<T>.pairwise(initialValue: T): Flow<WithPrev<T>> = pairwiseBy(initialValue, ::WithPrev)
|
||||||
|
|
||||||
|
/** Holds a [newValue] emitted from a [Flow], along with the [previousValue] emitted value. */
|
||||||
|
data class WithPrev<T>(val previousValue: T, val newValue: T)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new [Flow] that combines the [Set] changes between each emission from [this] using
|
||||||
|
* [transform].
|
||||||
|
*/
|
||||||
|
fun <T, R> Flow<Set<T>>.setChangesBy(
|
||||||
|
transform: suspend (removed: Set<T>, added: Set<T>) -> R,
|
||||||
|
): Flow<R> = onStart { emit(emptySet()) }.distinctUntilChanged()
|
||||||
|
.pairwiseBy { old: Set<T>, new: Set<T> ->
|
||||||
|
// If an element was present in the old set, but not the new one, then it was removed
|
||||||
|
val removed = old - new
|
||||||
|
// If an element is present in the new set, but on the old one, then it was added
|
||||||
|
val added = new - old
|
||||||
|
transform(removed, added)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a new [Flow] that produces the [Set] changes between each emission from [this]. */
|
||||||
|
fun <T> Flow<Set<T>>.setChanges(): Flow<SetChanges<T>> = setChangesBy(::SetChanges)
|
||||||
|
|
||||||
|
/** Contains the difference in elements between two [Set]s. */
|
||||||
|
data class SetChanges<T>(
|
||||||
|
/** Elements that are present in the first [Set] but not in the second. */
|
||||||
|
val removed: Set<T>,
|
||||||
|
/** Elements that are present in the second [Set] but not in the first. */
|
||||||
|
val added: Set<T>,
|
||||||
|
)
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.systemui.util.kotlin
|
||||||
|
|
||||||
|
import android.testing.AndroidTestingRunner
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import com.android.systemui.SysuiTestCase
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asFlow
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.merge
|
||||||
|
import kotlinx.coroutines.flow.takeWhile
|
||||||
|
import kotlinx.coroutines.flow.toList
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(AndroidTestingRunner::class)
|
||||||
|
class PairwiseFlowTest : SysuiTestCase() {
|
||||||
|
@Test
|
||||||
|
fun simple() = runBlocking {
|
||||||
|
assertThatFlow((1..3).asFlow().pairwise())
|
||||||
|
.emitsExactly(
|
||||||
|
WithPrev(1, 2),
|
||||||
|
WithPrev(2, 3),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun notEnough() = runBlocking {
|
||||||
|
assertThatFlow(flowOf(1).pairwise()).emitsNothing()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun withInit() = runBlocking {
|
||||||
|
assertThatFlow(flowOf(2).pairwise(initialValue = 1))
|
||||||
|
.emitsExactly(WithPrev(1, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun notEnoughWithInit() = runBlocking {
|
||||||
|
assertThatFlow(emptyFlow<Int>().pairwise(initialValue = 1)).emitsNothing()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun withStateFlow() = runBlocking(Dispatchers.Main.immediate) {
|
||||||
|
val state = MutableStateFlow(1)
|
||||||
|
val stop = MutableSharedFlow<Unit>()
|
||||||
|
|
||||||
|
val stoppable = merge(state, stop)
|
||||||
|
.takeWhile { it is Int }
|
||||||
|
.filterIsInstance<Int>()
|
||||||
|
|
||||||
|
val job1 = launch {
|
||||||
|
assertThatFlow(stoppable.pairwise()).emitsExactly(WithPrev(1, 2))
|
||||||
|
}
|
||||||
|
state.value = 2
|
||||||
|
val job2 = launch { assertThatFlow(stoppable.pairwise()).emitsNothing() }
|
||||||
|
|
||||||
|
stop.emit(Unit)
|
||||||
|
|
||||||
|
assertThatJob(job1).isCompleted()
|
||||||
|
assertThatJob(job2).isCompleted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(AndroidTestingRunner::class)
|
||||||
|
class SetChangesFlowTest : SysuiTestCase() {
|
||||||
|
@Test
|
||||||
|
fun simple() = runBlocking {
|
||||||
|
assertThatFlow(
|
||||||
|
flowOf(setOf(1, 2, 3), setOf(2, 3, 4)).setChanges()
|
||||||
|
).emitsExactly(
|
||||||
|
SetChanges(
|
||||||
|
added = setOf(1, 2, 3),
|
||||||
|
removed = emptySet(),
|
||||||
|
),
|
||||||
|
SetChanges(
|
||||||
|
added = setOf(4),
|
||||||
|
removed = setOf(1),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onlyOneEmission() = runBlocking {
|
||||||
|
assertThatFlow(flowOf(setOf(1)).setChanges())
|
||||||
|
.emitsExactly(
|
||||||
|
SetChanges(
|
||||||
|
added = setOf(1),
|
||||||
|
removed = emptySet(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fromEmptySet() = runBlocking {
|
||||||
|
assertThatFlow(flowOf(emptySet(), setOf(1, 2)).setChanges())
|
||||||
|
.emitsExactly(
|
||||||
|
SetChanges(
|
||||||
|
removed = emptySet(),
|
||||||
|
added = setOf(1, 2),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> assertThatFlow(flow: Flow<T>) = object {
|
||||||
|
suspend fun emitsExactly(vararg emissions: T) =
|
||||||
|
assertThat(flow.toList()).containsExactly(*emissions).inOrder()
|
||||||
|
suspend fun emitsNothing() =
|
||||||
|
assertThat(flow.toList()).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertThatJob(job: Job) = object {
|
||||||
|
fun isCompleted() = assertThat(job.isCompleted).isTrue()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user