# Mobile Data and State

**Stack:** any
**Layer:** frontend-data

## Intent

Data handling in a mobile app routes every use case through an Application handler, the one place where that use case is defined. Every piece of state carries a declared classification deciding whether it is persisted, kept on the device, or recomputed. This page states the mobile store, storage, and client those rules land in, following [Frontend Data and State](https://litespec.dev/4.0.0/frontend/data.md) (`litespec/frontend/data#route-use-cases-through-application`, `litespec/frontend/data#classify-state`, `litespec/frontend/data#update-the-cache-a-command-changed`), and a team that applies it while writing handlers and screens keeps one definition of each use case and one source of each value.

## Concepts

### The Three State Classifications

Durable state is what the user made: persisted, versioned, and exported. The expense report the visitor files is durable state, because the visitor built it and it must survive a restart and an export. Device-local state is persisted but stays on the device, excluded from export. Derived state is computed from durable state and never persisted: an expense total is derived from the report lines each time it is read.

## Standards

### Load Persisted State Once

**ID:** `litespec/frontend/mobile/data#load-persisted-state-once`

**Strength:** rule

**Statement:** Persisted state MUST load once at startup into a typed store that screens read from, with screens showing a loading state until it is ready.

**Rationale:** Storage reads are slow and fallible, so the product reads once at startup into a typed store that screens read from. A screen that reads storage on render pays an input-output call per frame and can render half-loaded state. A screen renders its loading state until the store finishes loading, so it never shows an unloaded store as ready.

**Reviewed for:** Which persisted state loads outside startup, or renders a screen before the typed store is ready? Look in the startup sequence and the screen loading states.

### Keep Persisted State in App-Private Storage

**ID:** `litespec/frontend/mobile/data#keep-persisted-state-in-app-private-storage`

**Strength:** rule

**Statement:** Persisted state and media the user adds MUST live in the app's private storage, written atomically.

**Rationale:** App-private storage keeps the user's data inside the app's own sandbox, away from other apps and from the shared media library. A crash mid-write leaves a half-written file, so each write lands in a temporary file that then replaces the target, keeping the previous complete file until the new one is whole. Media the user adds travels with the state that references it, so an export or a reinstall keeps both together.

**Example:** The filed expense report persists as versioned durable state in app-private storage, with the photographed receipts beside it in the same private files.

**Reviewed for:** Which persisted state or user-added media lives outside app-private storage, or writes without a temporary file? Look in the storage writes.

### Copy Picked Media into the App

**ID:** `litespec/frontend/mobile/data#copy-picked-media-into-the-app`

**Strength:** rule

**Statement:** Media picked from the device MUST be copied into app-private storage on import rather than kept as a reference to the picker's URI.

**Rationale:** A picker's URI is a loan, not a gift: the platform can revoke it when the picker closes, and the source file can move or vanish. A screen holding only the URI shows a broken image after either happens. Copying the media into app-private storage on import keeps the app's copy under the app's own control.

**Reviewed for:** Which imported media keeps a stored picker URI instead of a copy in app-private storage? Look in the media imports.

### Make Refusals Explicit

**ID:** `litespec/frontend/mobile/data#make-refusals-explicit`

**Strength:** rule

**Statement:** A handler MUST return a typed result union covering success and each named refusal, with each screen mapping every refusal to a message.

**Rationale:** Signaling a refusal through `null`, an empty collection, or a bare boolean tells the caller that something was refused but not what. Every caller then guesses, and two callers guess differently. A typed result union names success and each refusal, so each screen maps every refusal to the message the application holds for it. This realizes litespec/frontend/data#map-every-failure-to-one-problem-type.

**Reviewed for:** Which handler unions miss success or a named refusal, and which screens leave a refusal unmapped to a message? Look in the handler result types and the screens.

### Call the Server Only Through the Generated Client

**ID:** `litespec/frontend/mobile/data#call-the-server-only-through-the-generated-client`

**Strength:** rule

**Statement:** A `server` consumer's app MUST call the backend only through the generated client in `{ProjectName}.Client`.

**Rationale:** The generated client owns the base address, the credentials, the headers, and the mapping from a failure to a result. A call that builds its own request opts out of all four at once: it can point at the wrong host, miss the session, and surface a raw failure outside the handler's result shape. Routing every backend call through the one client keeps those decisions in one place. This realizes litespec/frontend/data#use-one-configured-client-per-application.

**Reviewed for:** Which backend calls in a `server` app bypass the generated client in `{ProjectName}.Client`? Look in the backend call sites.

## Conventions

### Refuse Commands While Offline

**ID:** `litespec/frontend/mobile/data#refuse-commands-while-offline`

**Strength:** default

**Default:** While the network is unavailable, a screen shows its failure state and queues no command; the visitor retries the command in the foreground when the network returns.

**Replacement:** A project records a departure as an explicit local convention naming each command it queues offline, its stored form, and its replay order.

**Rationale:** A queued command runs later against state the visitor cannot see, so an expense submitted on a train can land after its trip closed or its approval policy changed. Showing the failure state keeps the refusal where the visitor caused it, and queuing nothing keeps every command tied to the screen that sent it. A project that needs offline capture states that departure and its replay rules instead of letting each screen improvise a queue.

**Example:** Filing expense EXP-1042 for the Rotterdam client visit with no network shows the failure state on the expense screen and sends nothing; the visitor submits the expense again when the network returns.

**Reviewed for:** Which screens queue a command while offline without a recorded departure naming the queued commands and their replay order, and which offline screens show no failure state? Look in the command call sites and the offline screens.
