Edit and validate text
Use one stable TextFieldState for each logical editor. It is the authoritative owner of the rich
document, directional selection, active IME composition, and undo/redo history. This task covers
ordinary fields and search submission; the durable state and Android bridge invariants are defined
by the text input architecture.
Bind editable state
rememberTextFieldState preserves the document and selection through host recreation. Read its
observable properties directly instead of copying text into another callback-owned value.
fun UiTreeBuilder.EditableSearchForm(onSearch: (String) -> Unit) {
val query = rememberTextFieldState()
val name = rememberTextFieldState()
Column {
SearchBar(
state = query,
placeholder = "Search",
onSearch = onSearch,
)
TextField(
state = name,
label = "Display name",
supportingText = "Up to 24 characters",
inputTransformation = InputTransformation.maxCodePoints(24),
)
Button(
text = "Undo",
enabled = name.canUndo,
onClick = { name.undo() },
)
}
}
SearchBar selects the Search IME action only when onSearch is present and passes the latest
state.text. It does not debounce, clear, or submit by itself. TextField selects appearance,
input purpose, and line behavior without creating a second editor state.
Separate user and application edits
InputTransformation evaluates only platform-proposed edits. Compose transformations with then
when order matters; a later transformation sees the result of the earlier one. maxCodePoints
counts Unicode code points and therefore does not split a valid surrogate pair.
Application changes use one explicit state.edit { replace(0, length, replacement); selectAll() }
transaction.
One edit call publishes one state change and creates one undo unit. Selection-only changes do not
add history. Do not route programmatic edits through an input policy: application validation and
platform input filtering have different ownership.
Choose the component level
- Use
TextFieldfor labeled application forms and resolved design-system defaults. - Use
SearchBarfor a single-line query with optional Search submission. - Use
BasicTextFieldonly when a design system has already resolved a completeBasicTextFieldStyle; it intentionally performs no theme or component-Local lookup.
Use rich and received content when annotations, inline attachments, clipboard, drop, or IME content must survive editing.
Configure keyboard and IME actions
TextFieldInputProfile couples keyboard options and autofill semantics; TextFieldLinePolicy
separately owns visual line behavior. Use these values instead of password, email, number, or
text-area component wrappers.
fun UiTreeBuilder.EmailSubmissionField(onSubmit: (String) -> Unit) {
val email = rememberTextFieldState()
TextField(
state = email,
label = "Email",
inputProfile = TextFieldInputProfile(
keyboardOptions = TextFieldKeyboardOptions(
keyboardType = TextFieldType.Email,
imeAction = TextFieldImeAction.Done,
),
autofillHints = TextFieldInputProfile.Email.autofillHints,
),
onKeyboardAction = { action ->
if (action == TextFieldImeAction.Done) {
onSubmit(email.text)
true
} else {
false
}
},
)
}
Return true only for an action the application handled; false preserves native fallback. Keep
the profile stable unless product state changes because changing input type or editor options may
restart the active native connection. Equal recomposition must preserve selection and composition.
Verify the task
Compile with ./gradlew :samples:tutorials:compileDebugKotlin, then verify:
- type and select text; recomposition must not move the cursor or end active composition;
- exceed the code-point limit through the keyboard; the proposed edit must be rejected without a transient invalid value;
- invoke Search; the callback must receive the latest visible query;
- perform an application edit and undo it; document and selection must restore as one snapshot;
- confirm the expected keyboard, autofill category, and action; submit must read the latest text once while unhandled actions keep native fallback;
- recreate the Activity; document and selection restore, while composition and undo history do not.
A parallel String, lost cursor, stale submit value, transformation of an application edit, or
restored IME session is a failed integration.