Skip to content

Instantly share code, notes, and snippets.

@emenjivar
Last active June 7, 2025 17:38
Show Gist options
  • Save emenjivar/8d4f540a1e3bda443b445957efa9f727 to your computer and use it in GitHub Desktop.
Save emenjivar/8d4f540a1e3bda443b445957efa9f727 to your computer and use it in GitHub Desktop.
# Jetpack compose: Clean form field abstraction
package com.emenjivar.demohandlingerrorfields.inputs
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
sealed class InputField<T>(
initialValue: T
) {
abstract val name: String
abstract fun validate(input: T): String?
private val _value = MutableStateFlow(initialValue)
val value: StateFlow<T> = _value
private val _error = MutableStateFlow<String?>(null)
val error: StateFlow<String?> = _error
val isValid: Boolean
get() = validate(_value.value) == null
fun update(newValue: T) {
_value.update { newValue }
_error.value = validate(newValue)
}
}
package com.emenjivar.demohandlingerrorfields.inputs
import android.util.Patterns
class EmailInput : InputField<String>("") {
override val name = "name"
override fun validate(input: String): String? {
val isValidEmail = Patterns.EMAIL_ADDRESS.matcher(input).matches()
return if (!isValidEmail) {
"The email format is wrong"
} else {
null
}
}
}
class PasswordInput : InputField<String>("") {
override val name = "password"
override fun validate(input: String): String? {
return when {
input.length < 6 -> "The password must have at least 6 characters"
else -> null
}
}
}
class RestrictedAgeInput : InputField<Int>(0) {
override val name = "name"
override fun validate(input: Int): String? {
return if (input < 18) {
"The age must be +18"
} else {
null
}
}
}
@Composable
fun ScreenContent(uiState: UiState) {
val emailInput by uiState.emailInput.value.collectAsState()
val emailError by uiState.emailInput.error.collectAsState()
val passwordInput by uiState.passwordInput.value.collectAsState()
val passwordError by uiState.passwordInput.error.collectAsState()
val ageInput by uiState.ageInput.value.collectAsState()
val ageError by uiState.ageInput.error.collectAsState()
Column(modifier = Modifier.statusBarsPadding()) {
Text("Email")
TextField(
value = emailInput,
onValueChange = uiState.emailInput::update
)
emailError?.let { error ->
Text(text = error, color = Color.Red)
}
Text("Password")
TextField(
value = passwordInput,
onValueChange = uiState.passwordInput::update
)
passwordError?.let { error ->
Text(text = error, color = Color.Red)
}
Text("Age")
TextField(
value = ageInput.toString(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
onValueChange = {
it.toIntOrNull()?.let(uiState.ageInput::update)
}
)
ageError?.let { error ->
Text(text = error, color = Color.Red)
}
}
}
package com.emenjivar.demohandlingerrorfields
import androidx.lifecycle.ViewModel
import com.emenjivar.demohandlingerrorfields.inputs.EmailInput
import com.emenjivar.demohandlingerrorfields.inputs.InputField
import com.emenjivar.demohandlingerrorfields.inputs.PasswordInput
import com.emenjivar.demohandlingerrorfields.inputs.RestrictedAgeInput
class CustomViewModel : ViewModel() {
private val emailInput = EmailInput()
private val passwordInput = PasswordInput()
private val ageInput = RestrictedAgeInput()
val uiState = UiState(
emailInput = emailInput,
passwordInput = passwordInput,
ageInput = ageInput
)
}
data class UiState(
val emailInput: EmailInput,
val passwordInput: PasswordInput,
val ageInput: InputField<Int>
)
@emenjivar
Copy link
Author

output.webm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment