Merge "Flow utility functions: pairwise + setChanges" into tm-qpr-dev

This commit is contained in:
Steve Elliott
2022-08-29 18:50:35 +00:00
committed by Android (Google) Code Review
2 changed files with 239 additions and 0 deletions

View File

@@ -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>,
)

View File

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