Throttle UI actions instead of debouncing (#3651)

Introduce Flow<T>.throttleFirst(). In a flow this emits the first value,
and each value afterwards that is > some timeout after the previous
value.

This prevents accidental double-taps on UI elements from generating
multiple-actions.

The previous code used debounce(). That has a similar effect, but with
debounce() the code has to wait until after the timeout period has
elapsed before it can process the action, leading to an unnecessary
UI delay.

With throttleFirst a value is emitted immediately, there's no need
to wait. It's subsequent values that are potentially throttled.
This commit is contained in:
Nik Clayton 2023-06-11 13:34:22 +02:00 committed by GitHub
commit 5e8a63a046
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 131 additions and 5 deletions

View file

@ -40,6 +40,7 @@ import com.keylesspalace.tusky.usecase.TimelineCases
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.deserialize
import com.keylesspalace.tusky.util.serialize
import com.keylesspalace.tusky.util.throttleFirst
import com.keylesspalace.tusky.util.toViewData
import com.keylesspalace.tusky.viewdata.NotificationViewData
import com.keylesspalace.tusky.viewdata.StatusViewData
@ -52,7 +53,6 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
@ -65,6 +65,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.await
import retrofit2.HttpException
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.ExperimentalTime
data class UiState(
/** Filtered notification types */
@ -274,7 +276,7 @@ sealed class UiError(
}
}
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class, ExperimentalTime::class)
class NotificationsViewModel @Inject constructor(
private val repository: NotificationsRepository,
private val preferences: SharedPreferences,
@ -390,7 +392,7 @@ class NotificationsViewModel @Inject constructor(
// Handle NotificationAction.*
viewModelScope.launch {
uiAction.filterIsInstance<NotificationAction>()
.debounce(DEBOUNCE_TIMEOUT_MS)
.throttleFirst(THROTTLE_TIMEOUT)
.collect { action ->
try {
when (action) {
@ -409,7 +411,7 @@ class NotificationsViewModel @Inject constructor(
// Handle StatusAction.*
viewModelScope.launch {
uiAction.filterIsInstance<StatusAction>()
.debounce(DEBOUNCE_TIMEOUT_MS) // avoid double-taps
.throttleFirst(THROTTLE_TIMEOUT) // avoid double-taps
.collect { action ->
try {
when (action) {
@ -517,6 +519,6 @@ class NotificationsViewModel @Inject constructor(
companion object {
private const val TAG = "NotificationsViewModel"
private const val DEBOUNCE_TIMEOUT_MS = 500L
private val THROTTLE_TIMEOUT = 500.milliseconds
}
}