Hello, Form
Learn the basics of using Form. If you haven't completed the setup yet, please do so before proceeding. This tutorial covers the form package.
Step 1 - Form
First, use the rememberForm
API to obtain a Form
instance that manages form values and the state of each input field.
@Composable
fun App() {
MaterialTheme {
val form = rememberForm(
initialValue = "",
onSubmit = { value ->
println("onSubmit: $value")
}
)
}
}
The Form
infers the type from initialValue
. In this example, we create a simple form containing a single text input field by setting the initialValue
to a string type.
WARNING
If you expect state restoration on the Android platform, please ensure the type specified in initialValue
is restorable. The Form internally uses rememberSaveable
to manage form values, and runtime errors will be thrown for unsupported types.
Reference: RememberSaveable.kt;l=242
Step 2 - FormField
The association between the type specified in Form's initialValue
and each field is defined using FormField generation APIs: Form<T>.Field
or Form<T>.rememberField
.
@Composable
fun App() {
MaterialTheme {
val form = rememberForm(/* .. */)
Column {
form.Field(
selector = { it },
updater = { it },
validator = FieldValidator {
notBlank { "must not be blank" }
},
render = { field -> // field: FormField<String>
TextField(
value = field.value,
onValueChange = field::onValueChange,
modifier = Modifier.onFocusChanged { state ->
field.handleFocus(state.isFocused || state.hasFocus)
},
isError = field.hasError,
supportingText = {
if (field.hasError) {
Text(text = field.error.messages.first(), color = MaterialTheme.colorScheme.error)
}
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = {
// When focus doesn't move to the next field, you need to manually trigger validation
field.trigger(FieldValidationMode.Blur)
defaultKeyboardAction(ImeAction.Done)
})
)
}
)
}
}
}
In this example, we use the headless component Form<T>.Field
to connect the actual input component with the FormField
interface. The selector
extracts the field value from the form data, while the updater
specifies how to update the form data when the field changes.
The form package controls state and behavior but does not provide UI components. This allows maximum flexibility in designing your input components.
TIP
Built-in validation rules available within the FieldValidator
block are defined in form.rule. All validation rules are extension functions, so you can define custom validation rules within your project and use them in the validator block.
Step 3 - Submit
Form submission is controlled using Form<T>.handleSubmit
and can be combined with any component for flexible control.
@Composable
fun App() {
MaterialTheme {
val form = rememberForm(
initialValue = "",
onSubmit = { value ->
// This will only be called if validation passes
println("onSubmit: $value")
}
)
Column {
// ..
Button(
onClick = form::handleSubmit,
enabled = form.meta.canSubmit
) {
Text("Submit")
}
}
}
}
The handleSubmit
function automatically validates all fields and calls the onSubmit
callback only if validation passes. For controlling the submit button, meta.canSubmit
is useful as it indicates whether the form is ready for submission based on validation rules and form state.
INFO
Button controls during submission (after onSubmit
invocation) are outside the scope of the form package. Please effectively utilize processing states and other features provided by the query package's Mutation functionality.
Step 4 - FormPolicy
You can fine-tune when and how validation is executed with FormPolicy
.
@Composable
fun App() {
MaterialTheme {
val form = rememberForm(
initialValue = "",
policy = FormPolicy(
formOptions = FormOptions(
preValidationDelayOnChange = 300.milliseconds
),
fieldOptions = FieldOptions(
validationStrategy = FieldValidationStrategy(
initial = FieldValidationMode.Mount,
next = { current, isValid ->
if (isValid) FieldValidationMode.Blur else FieldValidationMode.Change
}
),
validationDelayOnChange = 300.milliseconds
)
),
onSubmit = { value -> println("onSubmit: $value") }
)
}
}
We provide preset policies to accommodate different UX requirements:
- FormPolicy() (Default) - Executes initial validation when fields lose focus, then performs delayed validation on each field value change
- FormPolicy.Minimal - Executes initial validation when the submit button is pressed (
meta.canSubmit
always returnstrue
)
Step 5 - FormState
In Step 1, we specified initial values directly in rememberForm
. Using FormState<T>
is helpful for more advanced scenarios or when integrating with external state management.
@Composable
fun App() {
MaterialTheme {
val formState = rememberFormState(
initialValue = ""
)
val formWithState = rememberForm(
state = formState,
onSubmit = { value ->
println("onSubmit: $value")
formState.reset("")
}
)
// For mutable state types (like TextFieldState)
val nameState = rememberTextFieldState()
val emailState = rememberTextFieldState()
val formMeta = rememberFormMetaState()
val customFormState = remember(formMeta.key) {
FormState(
value = MutableFormData(name = nameState, email = emailState),
meta = formMeta
)
}
val customForm = rememberForm(
state = customFormState,
onSubmit = { value -> println("onSubmit: $value") }
)
Column {
// Use any of the form instances created above
// ... fields and submit button
}
}
}
There are APIs that can only be called via FormState:
- reset: Returns all state within the Form to its initial state
- setError: Allows setting validation errors from outside the Form (e.g., reflecting validation errors from API submission results)
For typical scenarios, the rememberForm
-only approach is sufficient. You should use them selectively based on your requirements.
Finish 🏁
Do you understand the basics of using Form? This concludes the tutorial 🎊
If you wish to continue learning, try running the FormScreen
found in the sample code. If you have any concerns, please feel free to provide feedback on Github discussions.
Love the project? ⭐ it on GitHub and help us make it even better!