← Application platforms · Chapter 3

Build a recoverable mobile form with Compose

Build explicit draft, validation, persistence, and conflict states for a device inspection form, connecting Compose recomposition with Android lifecycle and offline recovery.

After this guide, you can

Implement persistent inspection drafts and a sync queue, testing rotation, process termination, offline submission, and preservation of edits during conflicts.

A device inspection form contains an equipment ID, a reading, and an exception note. Rotation, navigation, or lost connectivity should not silently discard saved input. This lesson uses a synthetic inspection record to organize a Compose screen around persistent local data.

The equipment identifier is teaching data. The application neither connects to industrial equipment nor sends control commands. Its initial scope is drafts and synchronization status, allowing input recovery, validation, and conflicts to be examined separately.

Model drafts and server versions

A draft contains a stable ID, equipment ID, field values, local edit sequence, last acknowledged server version, and pending-change status. Preserve measurement units. A reading of 12 without a unit is ambiguous. Define whether the exception note is optional or required rather than leaving the interface to infer the rule.

The form also represents saving, locally saved, failed, pending synchronization, and conflicted states. “Saved” means a completed local write. “Synced” means server acknowledgement. Field errors belong beside the relevant input, and fixing one field must not clear unrelated values.

Local drafts are authoritative for the interface. An older response must not replace unacknowledged edits. Send the known server version with a synchronization request and have the server enforce conditional updates. A mismatch produces a conflict, retaining both versions for comparison.

Understand recomposition and ownership

Compose recalculates the interface from observed state. Composable functions can execute repeatedly, so their bodies must not perform unconditional database writes or network requests. Input components receive values and callbacks. Events reach the state owner, which updates the data presented by the screen. This makes changes traceable.

remember retains state within composition; it does not guarantee recovery after process termination. rememberSaveable suits supported small UI values and is not storage for complete business records or large attachments. A ViewModel can organize screen state, but durable recovery still needs a repository and persistence.

Configuration changes and process death are separate tests. Restoring an expanded group and restoring an inspection draft have different requirements. Persist business fields first, then decide how focus and scrolling should survive the intended lifecycle transitions.

Establish the Android project

Create a Compose project in Android Studio and check the SDK, minimum target, and dependencies. Begin with record types, a validator, and an in-memory repository. Display inputs, exception notes, save status, and a submit action. Labels should include units, and errors should be accessible to assistive technology.

Then replace the repository with a local database. Room is an implementation option; configure its dependencies and interfaces using current documentation. Persist business fields and synchronization markers. A ViewModel exposes repository state to the screen, while events invoke saving. Blocking database or network work does not belong directly in UI callbacks.

Complete editing, saving, leaving, and reopening before adding a server. Confirm saving only after persistence succeeds. Retain input on failure and offer retry. Startup loads an existing draft or creates an empty form when none exists. A read failure must not be mistaken for absence and overwrite old data.

Persist writes and synchronization work

Treat saving the draft and adding its pending operation as one consistency boundary. Otherwise an updated draft can have no queued work, or queued work can reference old content. Store operation ID, draft ID, baseline version, and attempt state so unfinished work can be enumerated after relaunch.

Define what submit means. It can first move the local record to pending synchronization; confirmed success requires the server response. Offline work remains queued. Authentication failure needs a renewed session, while server validation failure returns the draft to an editable state. Repeated taps must not create an unlimited queue.

Background synchronization uses mechanisms supported by the target Android environment and remains subject to system and connectivity constraints. Do not promise completion at arbitrary intervals after exit. Reopening should examine durable pending work instead of depending on an old coroutine still existing.

Observe conflict preservation offline

The Python model compares a local revision with an incoming revision and refuses to overwrite a dirty draft when they differ. It implements no Kotlin, Compose, Room, or server-side conditional update.

local = {"revision": 2, "text": "local edit", "dirty": True}
remote = {"revision": 1, "text": "older"}
def merge(draft, incoming):
    if draft["dirty"] and incoming["revision"] != draft["revision"]:
        return "conflict"
    draft.update(incoming, dirty=False)
    return "synced"
assert merge(local, remote) == "conflict"
assert local["text"] == "local edit"
print(local)

The actual output is {'revision': 2, 'text': 'local edit', 'dirty': True}. The older response leaves the local input intact. This model uses a simplified revision field. A real protocol should distinguish local edit sequences from server baseline versions; numbers from separate version systems cannot be compared as one ordered sequence.

A conflict screen should show local content, server content, and explicit resolution actions. A chosen merge becomes a new local edit using the current server version as its baseline. Do not apply unconfirmed overwrites. A timestamp-based rule also needs analysis of clock skew and field meaning.

Handle recovery and duplicate attempts

Terminate the process during saving and check that storage contains a complete old or new record, without partial fields. Relaunch restores the last successful save and explains unfinished work. For attachments, complete and validate the file before referencing it from a record.

A timeout establishes that a response did not arrive promptly, not that the server performed no update. Retry with the same operation ID and require server idempotency. An acknowledgement must match the current pending operation, so an old response cannot mark later edits as synchronized.

Allow valid intermediate input states. A minus sign or unfinished decimal should not immediately become zero. Validate the completed value for submission while retaining the entered text for correction.

Exercise: verify a recoverable form

Create two drafts with reading bounds and exception-note rules. Inject local-write failure, offline operation, server version changes, and repeated submission. Values must survive failure, retries must not duplicate business records, and conflicts must preserve local edits.

Test rotation and process termination in the emulator, then record offline recovery on the target device. Compare screen values, stored drafts, and acknowledged server versions. Temporary differences are valid only when status explains them. Mark only acknowledged content as synchronized. The Python assertions do not replace this Android engineering validation.

Sources & further reading

Sources checked on 2026-11-10. Original engineering explanation; examples are teaching data, not production measurements.