Skip to content

Instantly share code, notes, and snippets.

@nemscep
Last active November 23, 2023 21:55
Show Gist options
  • Save nemscep/793cbcb90371c32f9f75886fd3c002e0 to your computer and use it in GitHub Desktop.
Save nemscep/793cbcb90371c32f9f75886fd3c002e0 to your computer and use it in GitHub Desktop.
TextField which automatically triggers provided `onValueChange` lambda with the full text input (not for every character).
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.TextField
import androidx.compose.material.TextFieldColors
import androidx.compose.material.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.transformLatest
@Composable
fun TextFieldWithBackoff(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current,
label: @Composable (() -> Unit)? = null,
placeholder: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
isError: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(),
singleLine: Boolean = false,
maxLines: Int = Int.MAX_VALUE,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape =
MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
colors: TextFieldColors = TextFieldDefaults.textFieldColors(),
changeTriggerTime: Long = 1500L
) {
var inputText by remember { mutableStateOf(value) }
val inputStateFlow = remember {
MutableStateFlow<InputFieldState<String>>(InputChanged(inputText))
}
// Set key1 as true indicating that the behaviour is not reoccurring after recomposition.
LaunchedEffect(key1 = true) {
// Observe input field state.
// For every input change start a timer and wait for it to expire.
// Once expired, move to idle state which triggers onValueChange call.
inputStateFlow
.transformLatest { state ->
when (state) {
Idle -> emit(inputText)
is InputChanged -> {
delay(changeTriggerTime)
inputStateFlow.value = Idle
}
}
}
.collect { value -> onValueChange(value) }
}
TextField(
enabled = enabled,
readOnly = readOnly,
value = value,
onValueChange = {
if (value != inputText) {
inputText = value
inputStateFlow.value = InputChanged(inputText)
}
},
modifier = modifier,
singleLine = singleLine,
textStyle = textStyle,
label = label,
placeholder = placeholder,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
isError = isError,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
maxLines = maxLines,
interactionSource = interactionSource,
shape = shape,
colors = colors
)
}
/**
* Sealed class representing all text input states.
*/
sealed class InputFieldState<out T> {
object Idle : InputFieldState<Nothing>()
data class InputChanged<T>(val value: T) : InputFieldState<T>()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment