Getting started
Rules
Validation is composed from ValueValidatorRules. Built-ins cover the common cases —
Required, Email, MinLength(n), MaxLength(n), Phone(region) (see
phone validation), HexColor, OneOf(allowed), typed numeric
Min/Max/InRange, and more in com.chrisjenx.yakcov.strings /
com.chrisjenx.yakcov.generic.
Apply any rule conditionally with onlyWhen: Required.onlyWhen(isBusiness) runs the
rule only while the flag is true and passes otherwise.
ValueValidatorRule is a fun interface, so one-off rules are a lambda away:
// ValueValidatorRule is a fun interface — one-off rules can be inline SAM lambdas.
// Severity is graded: error/warning/info/success all flow through supportingText.
val NoPlusAddressing = ValueValidatorRule<String> { value ->
if ("+" in value) RegularValidationResult.warning("Plus-addressing may break receipts")
else RegularValidationResult.success()
}
See the built-in rules reference for the full catalog, and custom rules for reusable rule types and severity grading.
Two validator families
| In-composition | State-holder-owned (headless) | |
|---|---|---|
| Create with | rememberTextFieldValueValidator(rules) / rememberGenericValueValidator(state, rules) |
FieldValidator(initial, rules) — plain constructor |
| Choose when | The form has no state holder; everything lives in the composable | A ViewModel/presenter owns form state; you want submit() testable without UI |
Both are snapshot-state backed, so composables that read them recompose automatically. The pattern guides (ViewModel, MVI, Circuit) show which to reach for in each architecture.
Showing errors
Validation severity and error display are separate channels: rules compute a severity on every change, but nothing is shown until the validator decides errors should be visible (focus lost, submit, …). That's why nothing flashes red while the user is still typing.
isError()/FieldValidationState.isError— true once errors are shown and severity isERRORsupportingText()— a ready-made slot value for MaterialTextFields; renders the most severe message, ornullwhen nothing should show. PasssupportingText(default = "…")to show a plain hint while the field has no message (e.g. a pristine field); the samedefaultworks onFieldValidationState.text()/supportingText()Outcomeseverities:ERROR>WARNING>INFO>SUCCESS. Warnings and info messages flow through the same display channel without blocking submission.
validationConfig
For in-composition validators, the validationConfig modifier wires interaction
behavior:
modifier = Modifier.validationConfig(
validateOnFocusLost = true, // start validating when focus leaves the field
shakeOnInvalid = true, // shake the field when validate() fails
showErrorOnInteraction = false, // defer isError until validate() is called
)
For headless FieldValidators there's no validationConfig modifier; call
field.onFocusLost() from Modifier.onFocusLost { ... } to validate-on-focus-lost,
state-holder owned. The shakeOnInvalid / showErrorOnInteraction knobs are specific to
the in-composition validators — on the headless path, drive shake yourself (see
MVI) and gate error display via the showError flag you thread
through toFieldState.
Form-level submit
Validate everything at once; errors surface on every field and the call tells you whether to proceed:
Works on both families: List<ValueValidator<*, *>>.validate() and
List<FieldValidator<*>>.validate().
A complete screen
@Composable
fun BasicSignUpScreen(onSubmit: (email: String, password: String) -> Unit) {
// No state holder: the validators live in the composable, remembered across
// recompositions. Each one owns its TextFieldValue draft + validation state.
val email = rememberTextFieldValueValidator(rules = listOf(Required, Email))
val password = rememberTextFieldValueValidator(rules = listOf(Required, MinLength(8)))
Column {
with(email) {
OutlinedTextField(
value = value,
onValueChange = ::onValueChange,
label = { Text("Email") },
isError = isError(),
supportingText = supportingText(),
singleLine = true,
modifier = Modifier
.fillMaxWidth()
// Show errors when focus leaves the field; shake on invalid submit.
.validationConfig(validateOnFocusLost = true, shakeOnInvalid = true),
)
}
with(password) {
OutlinedTextField(
value = value,
onValueChange = ::onValueChange,
label = { Text("Password") },
isError = isError(),
supportingText = supportingText(),
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.validationConfig(validateOnFocusLost = true, shakeOnInvalid = true),
)
}
Button(
onClick = {
// Form-level validate(): surfaces errors on every field, true when all pass.
if (listOf(email, password).validate()) {
onSubmit(email.value.text, password.value.text)
}
},
modifier = Modifier.fillMaxWidth(),
) { Text("Create account") }
}
}