Add Flow.throttleLatest extension.

This commit is contained in:
Greyson Parrelli 2024-09-16 08:20:19 -04:00
parent 6b9e921888
commit c36c6e62e2
6 changed files with 184 additions and 6 deletions

View file

@ -10,17 +10,14 @@ import androidx.compose.runtime.Composable
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.flowWithLifecycle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import org.signal.core.util.throttleLatest
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatus
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData
import org.thoughtcrime.securesms.banner.Banner
@ -67,8 +64,7 @@ class MediaRestoreProgressBanner(private val data: MediaRestoreEvent) : Banner()
return flow
.flowWithLifecycle(lifecycleOwner.lifecycle)
.buffer(1, BufferOverflow.DROP_OLDEST)
.onEach { delay(1.seconds) }
.throttleLatest(1.seconds)
.map { MediaRestoreProgressBanner(loadData()) }
.flowOn(Dispatchers.IO)
}

View file

@ -25,7 +25,11 @@ kotlin {
dependencies {
implementation(libs.kotlin.reflect)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.core.jvm)
testImplementation(testLibs.junit.junit)
testImplementation(testLibs.assertj.core)
testImplementation(testLibs.junit.junit)
testImplementation(testLibs.kotlinx.coroutines.test)
}

View file

@ -0,0 +1,24 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.util
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.onEach
import kotlin.time.Duration
/**
* Throttles the flow so that at most one value is emitted every [timeout]. The latest value is always emitted.
*
* You can think of this like debouncing, but with "checkpoints" so that even if you have a constant stream of values,
* you'll still get an emission every [timeout] (unlike debouncing, which will only emit once the stream settles down).
*/
fun <T> Flow<T>.throttleLatest(timeout: Duration): Flow<T> {
return this
.conflate()
.onEach { delay(timeout) }
}

View file

@ -0,0 +1,63 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.util
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
import kotlin.time.Duration.Companion.milliseconds
class FlowExtensionsTests {
@Test
fun `throttleLatest - always emits first value`() = runTest {
val testFlow = flow {
delay(10)
emit(1)
}
val output = testFlow
.throttleLatest(100.milliseconds)
.toList()
assertEquals(listOf(1), output)
}
@Test
fun `throttleLatest - always emits last value`() = runTest {
val testFlow = flow {
delay(10)
emit(1)
delay(30)
emit(2)
}
val output = testFlow
.throttleLatest(20.milliseconds)
.toList()
assertEquals(listOf(1, 2), output)
}
@Test
fun `throttleLatest - skips intermediate values`() = runTest {
val testFlow = flow {
for (i in 1..30) {
emit(i)
delay(10)
}
}
val output = testFlow
.throttleLatest(50.milliseconds)
.toList()
assertEquals(listOf(1, 5, 10, 15, 20, 25, 30), output)
}
}

View file

@ -47,6 +47,8 @@ dependencyResolutionManagement {
library("kotlin-reflect", "org.jetbrains.kotlin", "kotlin-reflect").versionRef("kotlin")
library("kotlin-gradle-plugin", "org.jetbrains.kotlin", "kotlin-gradle-plugin").versionRef("kotlin")
library("ktlint", "org.jlleitschuh.gradle:ktlint-gradle:12.1.1")
library("kotlinx-coroutines-core", "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
library("kotlinx-coroutines-core-jvm", "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0")
library("kotlinx-coroutines-play-services", "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.8.1")
library("kotlinx-coroutines-rx3", "org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.3.9")
@ -196,6 +198,7 @@ dependencyResolutionManagement {
library("androidx-test-orchestrator", "androidx.test:orchestrator:1.4.1")
library("androidx-test-runner", "androidx.test", "runner").versionRef("androidx-test")
library("espresso-core", "androidx.test.espresso", "espresso-core").versionRef("espresso")
library("kotlinx-coroutines-test", "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
library("mockito-core", "org.mockito:mockito-inline:4.6.1")
library("mockito-kotlin", "org.mockito.kotlin:mockito-kotlin:4.0.0")
library("mockito-android", "org.mockito:mockito-android:4.6.1")

View file

@ -7968,6 +7968,17 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="f482314b5079c1455f6fb0d4257a745d101c6124ce961522ba86f9dc90901e47" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="2.0.0">
<artifact name="kotlin-stdlib-2.0.0-all.jar">
<sha256 value="30e05222bc067fffb896a3276b5f1e3f29450bf1d9461d27a19ce0efc793b5cd" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-stdlib-2.0.0.jar">
<sha256 value="240938c4aab8e73e888703e3e7d3f87383ffe5bd536d6d5e3c100d4cd0379fcf" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-stdlib-2.0.0.module">
<sha256 value="64f96ea8e7b9896731052241ffd3a265f8274d761e5fe9dc088ac45b31718341" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-stdlib-common" version="1.4.21">
<artifact name="kotlin-stdlib-common-1.4.21.jar">
<sha256 value="812cf197d9c4c67e1f47f95e2d72a9b600f0d1124560617bfe9850773eccbcff" origin="Generated by Gradle"/>
@ -8043,6 +8054,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="f93c9e9abf8d52d8e8fd8e851aa802ecec55132161c4aeee7d3cd924bf794246" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-stdlib-common" version="2.0.0">
<artifact name="kotlin-stdlib-common-2.0.0.module">
<sha256 value="2335187440c51d0d69e1b906fefc31f6169691c8598177b0e610c9b9a92ce6b5" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk7" version="1.4.10">
<artifact name="kotlin-stdlib-jdk7-1.4.10.jar">
<sha256 value="f9566380c08722c780ce33ceee23e98ddf765ca98fabd3e2fabae7975c8d232b" origin="Generated by Gradle"/>
@ -8291,6 +8307,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="f00af0920cb7dc4611205ff592eec5aa0071bc959b3797026abfe43cf14bfad4" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-android" version="1.9.0">
<artifact name="kotlinx-coroutines-android-1.9.0.jar">
<sha256 value="bd783acd2f9738845d58380f46f45b5cde95d0cb03027d56725338b26bfc4d72" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlinx-coroutines-android-1.9.0.module">
<sha256 value="7df1e04d77f0cc411890d999a9449b0e3bcd4f159a46c3060b110204ca607d43" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core" version="1.5.0">
<artifact name="kotlinx-coroutines-core-1.5.0.module">
<sha256 value="d8a26a896da32fb1f8c3f13fe41cb798a612a1c1ddf3a555d82ee1ff16ef13d3" origin="Generated by Gradle"/>
@ -8346,6 +8370,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="daf50f1c9404b224a1d6dd5286f8e8ee7d63fe807f78ea98f71795c183b6025f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core" version="1.9.0">
<artifact name="kotlinx-coroutines-core-1.9.0.module">
<sha256 value="ad534034a953b4e12cbeeb874c66adf8b1ca14df15fe0d2e6547aa34e86dfeca" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlinx-coroutines-core-metadata-1.9.0.jar">
<sha256 value="7089c33c145865020760d3dbca5e4634133cc3dd7feb926e830f6de6ede28ac6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core-jvm" version="1.5.0">
<artifact name="kotlinx-coroutines-core-jvm-1.5.0.jar">
<sha256 value="78d6cc7135f84d692ff3752fcfd1fa1bbe0940d7df70652e4f1eaeec0c78afbb" origin="Generated by Gradle"/>
@ -8399,6 +8431,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="09b81c9d11c2deebf133ad87b707927c1f04099cb611ef008c7725b3eb308329" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core-jvm" version="1.9.0">
<artifact name="kotlinx-coroutines-core-jvm-1.9.0.jar">
<sha256 value="ad89c2892235e670f222d819cb3d81188143cb19a05b59df9889ae4269f5c70a" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlinx-coroutines-core-jvm-1.9.0.module">
<sha256 value="b321a899e40d3ce345707aa2cfda9983ad0dcc69fea74a9b8bf906a16c1cf8a9" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-play-services" version="1.8.1">
<artifact name="kotlinx-coroutines-play-services-1.8.1.jar">
<sha256 value="f90c2c76cab2fb3db89bf8a742bb4f36cba43e52472aeff3919e9143bd5ec072" origin="Generated by Gradle"/>
@ -8407,6 +8447,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="2eb0b5c00791576c33079a0750ef3ded335c180f60043d51a19c799f8b737bc5" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-play-services" version="1.9.0">
<artifact name="kotlinx-coroutines-play-services-1.9.0.jar">
<sha256 value="4cb6b6321df1664b99fee756de63f6af65ccb4f68936f26cf76c211dea1b23ae" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlinx-coroutines-play-services-1.9.0.module">
<sha256 value="93dcee9a831a2f9dcda9528bab28d724e15e8a0bfec4194ef81d4ec7e7301ff2" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-reactive" version="1.3.9">
<artifact name="kotlinx-coroutines-reactive-1.3.9.module">
<sha256 value="afa2e53b31073b3888dc2bb526dc78cf09d78ba5993a1f12f71ff02d94b4abd0" origin="Generated by Gradle"/>
@ -8420,6 +8468,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="9090472f51918695de4084e8d6edf186b1d2f90babe063629f03d313e5d67e6b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-reactive" version="1.9.0">
<artifact name="kotlinx-coroutines-reactive-1.9.0.jar">
<sha256 value="4e889ac740e0ba9e6d3a429afc1a908c5a172649d0733cc56bdb6cafc59384d4" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlinx-coroutines-reactive-1.9.0.module">
<sha256 value="7d2d62b887bd6fe9811cb4e553551e32f5954f58d46ce588e34e954946f60c5c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-rx3" version="1.3.9">
<artifact name="kotlinx-coroutines-rx3-1.3.9.module">
<sha256 value="717591b07c7875391cd592779cd60b2a2f87d2a7e556f2e94d3459fb1e413b1b" origin="Generated by Gradle"/>
@ -8433,6 +8489,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="c149e9405417ece9ab4207aad5990c3c6f42776fc750c49a27ea99e32a738264" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-rx3" version="1.9.0">
<artifact name="kotlinx-coroutines-rx3-1.9.0.jar">
<sha256 value="f117db86962fa918ae20a8ef2668d5797123d957b1c2c5691ba375f7e5a8d7d9" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlinx-coroutines-rx3-1.9.0.module">
<sha256 value="ed4c120c4904bbe73c38c431eab109a7ef8e5de1ababa8fd87445915b0ad59db" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-swing" version="1.8.1">
<artifact name="kotlinx-coroutines-swing-1.8.1.jar">
<sha256 value="0c809e79dc64ab5bc9b790389fe491cc19d3574345a431276966fbc09994a659" origin="Generated by Gradle"/>
@ -8441,6 +8505,30 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="2915188bf6dde89157cccb480bfa1249f010917a8e1fad340c9dbef6d7e0229b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-swing" version="1.9.0">
<artifact name="kotlinx-coroutines-swing-1.9.0.jar">
<sha256 value="ce506d82f3642651a39fc706f3d2928d846772a64c92e749f018d7f0f216826f" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlinx-coroutines-swing-1.9.0.module">
<sha256 value="6b04c3d5e7b1a4bfe2813fc3039393c4fe5dda7f03d75d703ca23efb79de79c7" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-test" version="1.9.0">
<artifact name="kotlinx-coroutines-test-1.9.0.module">
<sha256 value="3a5f38700b2a4eb290e3507a0e3e4f9cc0bd63e70cca7f15e911dbf37c4f44ca" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlinx-coroutines-test-metadata-1.9.0.jar">
<sha256 value="b9f0603718765fe9f93b4a139198ada254f11c7bf18e48c99629a3a73ce281d1" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-test-jvm" version="1.9.0">
<artifact name="kotlinx-coroutines-test-jvm-1.9.0.jar">
<sha256 value="68b87fa90db3dab1e794ff6078364087c07f87e5e4a4d0c8d3272a222ee8fe7e" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlinx-coroutines-test-jvm-1.9.0.module">
<sha256 value="e2364beb540720b3c117957853379d4ae222058ad88f73a2ac73026c836f839f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-serialization-core" version="1.0.1">
<artifact name="kotlinx-serialization-core-1.0.1.module">
<sha256 value="9d0f1f6db25e394c89d58838eaebf3eabe360ef8ceb98aa3b9a4283532a54077" origin="Generated by Gradle"/>