Android development / Basic: Android and Compose foundations / Lesson 4
Compose state, events, and recomposition
Build an editable reading title by making state ownership visible and keeping rendering separate from effects.
On this page
Lesson goals
- Explain recomposition and state observation.
- Hoist state and choose an appropriate state lifetime.
Describe the UI from current values
A composable describes UI for its current inputs. When observed state changes, Compose can rerun affected functions and update the composition. Recomposition may happen frequently, may skip functions, and is not a promise that every statement executes exactly once. Therefore a composable body should not send requests, write files, or increment a global counter merely because it is rendered. Think of the screen as a projection of state, and user gestures as events that ask an owner to produce a new state.
Keep local editable state observable
An ordinary local variable is recreated when its function executes and does not notify Compose when assigned. remember retains an object while its call remains in the composition, and mutableStateOf makes reads observable. rememberSaveable additionally saves eligible small values through the saved-state mechanism. It is useful for a text draft, not a replacement for a database or a guarantee after every termination scenario. A remembered object disappears when its owner leaves the composition; choosing the owner is therefore as important as choosing the state API.
@Composable
fun TitleEditor() {
var title by rememberSaveable { mutableStateOf("") }
TitleField(title = title, onTitleChange = { title = it })
}
Use Compose runtime getValue and setValue imports, mutableStateOf, and the saveable rememberSaveable API in the generated Compose project. TitleField is defined below. This state survives supported saved-state restoration, but confirmed records still belong in durable storage.
Expose values and events
State hoisting moves a value to the lowest common owner that needs to read or change it. A reusable field receives its current text and an onTitleChange callback instead of creating a second private copy. The parent can then validate, save, or share that value with a preview. This establishes a single direction: values flow down and events flow up. Avoid passing an entire screen controller into every small control; a narrow callback makes the component easier to preview and test without a real activity or data layer.
@Composable
fun TitleField(title: String, onTitleChange: (String) -> Unit) {
OutlinedTextField(
value = title,
onValueChange = onTitleChange,
label = { Text(stringResource(R.string.reading_title)) },
singleLine = true
)
}
Add reading_title to the app string resources and import Material components and stringResource. The parent remains the source of truth. The field reports proposed text rather than mutating a model behind the parentโs back.
Give effects explicit lifetimes
An effect is work tied to composition or lifecycle rather than UI description. LaunchedEffect starts a coroutine when it enters composition and restarts it when its keys change. DisposableEffect pairs registration with cleanup. Choose keys that represent the identity of the work, such as an item id, rather than a random value that changes every render. Saving a record is usually a user event handled by a state owner, not an automatic consequence of showing its title. Durable operations need stronger ownership than a disappearing composable.
Publish changes rather than hiding mutation
Compose must observe the state that changes. Mutating a plain MutableList inside a remembered holder may leave readers uninformed; replace an immutable list or use a deliberate snapshot-aware collection. Calculate simple derived values directly from inputs, reserving derivedStateOf for cases where it reduces unnecessary updates. Keep expensive work out of the rendering path. When reviewing behavior, ask which state read triggers this UI and which event changes that state. If those answers are unclear, the ownership contract needs attention before adding more effects.
Common errors and fixes
Typing does not update the field
- Why it happens
- The text is stored in an ordinary variable or a disconnected copy.
- How to fix it
- Use observable state owned by the parent and feed its value back.
- Prevent it next time
- Trace the complete value-event-value loop during review.
A request repeats on every render
- Why it happens
- Side effects run directly in a composable body.
- How to fix it
- Move work to an event handler or correctly keyed effect.
- Prevent it next time
- Keep rendering functions free of uncontrolled external mutation.
A draft disappears when switching screens
- Why it happens
- Its remembered owner left the composition.
- How to fix it
- Move ownership to the appropriate screen state holder or durable draft store.
- Prevent it next time
- Write down the required lifetime before selecting an API.
Check your understanding
1. Two sibling controls edit and preview the same title. Where should the title live, and what should each child receive?
Show worked solution
Keep one title in their closest shared owner. Give the editor the value and an update callback; give the preview only the value. The preview needs no mutation authority. A validation message can be derived from the same title so all three pieces agree immediately after each event.
2. A loading effect uses the current clock time as its key. Explain the failure and choose a better key.
Show worked solution
The key changes across renders, so work is repeatedly canceled and restarted. Use the stable item id when the load belongs to an item, and move business loading into the screen state holder when it must survive composition changes. Cancellation must remain cooperative and should not be treated as an ordinary failure.
Key takeaways
- State ownership determines both consistency and lifetime.
- Recomposition is safe when rendering is predictable.
- Events request changes; reusable controls need narrow contracts.