Last active
June 7, 2025 17:38
-
-
Save emenjivar/8d4f540a1e3bda443b445957efa9f727 to your computer and use it in GitHub Desktop.
# Jetpack compose: Clean form field abstraction
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
output.webm