Flutter architecture guide
Flutter offline-first architecture
Use a repository as the single application boundary for local and remote data. Let the interface render explicit state while durable services own storage, synchronisation, retry and conflict behaviour.
The short answer
A maintainable Flutter offline-first architecture separates views, view models, repositories and services. A feature reads state through its repository. The repository coordinates a local database service, remote API service and synchronisation logic, then exposes one coherent application model regardless of connectivity.
The local database can be the source from which the interface reads, while the server remains authoritative for shared business rules, access and accepted outcomes. The repository reconciles those responsibilities.
01
Use clear UI, data and optional domain layers
| Component | Responsibility | Avoid |
|---|---|---|
| View | Render state and capture user intent | Database or API decisions |
| View model | Coordinate the feature and expose UI state | Duplicated persistence rules |
| Use case | Combine repositories for complex business action | Boilerplate for every simple call |
| Repository | Own application data and offline policy | Flutter widget dependencies |
| Service | Wrap one database, API or platform capability | Product state and cross-service rules |
02
Make synchronisation state part of the model
A boolean such as isOffline cannot describe the real workflow. The device may be connected while the API is unavailable, or a request may time out after the server accepted it.
Local
Saved
The intended change is durable on the device.
Queue
Pending
The operation is waiting for a suitable delivery attempt.
Remote
Confirmed
The server accepted and returned authoritative state.
Review
Failed or conflicted
Automatic retry stopped and the user or operator has a next action.
Model record state and operation state separately. A record can remain readable while one change to it is pending or conflicted.
03
Keep offline policy inside repository contracts
Define abstract repository interfaces around business data, not storage packages. A repository method can expose a stream of current records, create a local operation, request refresh or resolve a conflict without revealing whether SQL, files or a remote API performed the work.
- make the repository the only mutation boundary for its data type;
- write permitted offline changes and outbox entries atomically;
- map local, remote and domain models deliberately;
- apply remote updates through the same controlled boundary;
- return typed validation, conflict and retryable failures; and
- inject repository implementations for test, staging and production environments.
Read the broader Flutter app development guide for UI, data and native integration responsibilities.
04
Choose storage from data behaviour and risk
| Requirement | Storage question |
|---|---|
| Relational records | Are joins, indexes, transactions and migrations important? |
| Pending operations | Can an outbox entry and local state change commit together? |
| Sensitive data | What encryption, key storage, minimisation and expiry are required? |
| Large files | Should metadata and file transfer state be stored separately? |
| Platform coverage | Does the package support every deployed platform and build toolchain? |
| Ownership | Is the dependency maintained, testable and replaceable? |
Protect tokens through platform-supported secure storage. Do not assume that choosing an encrypted database makes the API, identity flow or local export behaviour secure.
05
Treat background synchronisation as a platform integration
The synchronisation service drains durable work when conditions allow. It should not depend on a widget, route or in-memory timer remaining alive. Flutter can call platform capabilities through plugins or platform channels, but iOS and Android control background opportunities.
- Select eligible outbox operations in a stable order.
- Refresh identity if allowed, or pause for sign-in.
- Send an operation ID, record version and intended command.
- Classify the response as confirmed, rejected, conflicted or retryable.
- Apply the result to local records and operation state atomically.
- Schedule the next attempt with bounded backoff.
- Notify the interface through the repository's observable state.
The detailed protocol is covered in offline mobile app synchronisation.
06
Test components separately and the distributed story together
- unit-test service mapping, repository rules and conflict policies;
- test view models against fake repository states;
- widget-test pending, stale, failed and conflict experiences;
- integration-test database migrations and atomic outbox writes;
- contract-test API request, error and version behaviour;
- terminate the process between local save and network delivery;
- change identity or permission while work is pending;
- deliver duplicate and out-of-order responses; and
- run long-offline, upgrade and recovery tests on representative devices.
07
Build one offline vertical slice in this order
- Write the offline product contract.
- Define domain records, versions and operation states.
- Create abstract repository and service interfaces.
- Implement the local store and atomic outbox write.
- Render current and pending state through the view model.
- Connect one command to the authenticated business API.
- Add retry, duplicate handling and one real conflict policy.
- Test platform lifecycle and background constraints.
- Pilot with representative network and device conditions.
Extend the pattern only after the first feature is observable, recoverable and understandable to users and support staff.
Sources
Primary Flutter and platform references
Questions
Frequently asked questions
How should a Flutter offline-first app be structured?
Keep views and view models in the UI layer, put application data and offline rules behind repositories, wrap the local database and remote API in services, and use a durable synchronisation worker to move operations between them.
Should Flutter screens call the API directly?
No. Screens should observe explicit application state from a view model. Repositories should coordinate local and remote services so features behave consistently across connection states.
What is the source of truth in a Flutter offline-first app?
For an offline-capable feature, the repository is the application-level source of truth and commonly exposes state backed by the local database. The server remains authoritative for shared business outcomes and permissions.
Which Flutter database should we use?
Choose from the data model, query, transaction, migration, encryption, platform and maintenance requirements. Validate the package and native dependencies rather than choosing from popularity alone.
Can Flutter synchronise in the background?
Yes, through platform capabilities and suitable plugins or native integration, but iOS and Android control when background work runs. Design delayed, interrupted and resumed synchronisation as normal behaviour.
Validate the architecture
Bring the Flutter journeys, local data, APIs, conflict rules and platform background requirements.
LCR can prototype the riskiest offline slice and define the production architecture around real device conditions.