Skip to content

Basic screen

When to use: quick forms and screens with no state holder — the validators live directly in the composable.

@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") }
    }
}

How the wiring works:

  • rememberTextFieldValueValidator keeps each validator across recompositions. The validator owns the field's TextFieldValue draft and its validation state — no separate mutableStateOf needed.
  • Modifier.validationConfig(validateOnFocusLost = true, shakeOnInvalid = true) starts validation when the user leaves the field and shakes it when a submit finds it invalid. Nothing turns red while they're still typing.
  • The submit button gates on listOf(email, password).validate() — this surfaces errors on every field (including ones the user never touched) and returns true only when all rules pass.

When the form grows a real state holder, graduate to the ViewModel, MVI, or Circuit wiring.