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

ViewView modelUse case where neededRepositoryLocal and remote services
ComponentResponsibilityAvoid
ViewRender state and capture user intentDatabase or API decisions
View modelCoordinate the feature and expose UI stateDuplicated persistence rules
Use caseCombine repositories for complex business actionBoilerplate for every simple call
RepositoryOwn application data and offline policyFlutter widget dependencies
ServiceWrap one database, API or platform capabilityProduct 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

RequirementStorage question
Relational recordsAre joins, indexes, transactions and migrations important?
Pending operationsCan an outbox entry and local state change commit together?
Sensitive dataWhat encryption, key storage, minimisation and expiry are required?
Large filesShould metadata and file transfer state be stored separately?
Platform coverageDoes the package support every deployed platform and build toolchain?
OwnershipIs 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.

  1. Select eligible outbox operations in a stable order.
  2. Refresh identity if allowed, or pause for sign-in.
  3. Send an operation ID, record version and intended command.
  4. Classify the response as confirmed, rejected, conflicted or retryable.
  5. Apply the result to local records and operation state atomically.
  6. Schedule the next attempt with bounded backoff.
  7. 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

  1. Write the offline product contract.
  2. Define domain records, versions and operation states.
  3. Create abstract repository and service interfaces.
  4. Implement the local store and atomic outbox write.
  5. Render current and pending state through the view model.
  6. Connect one command to the authenticated business API.
  7. Add retry, duplicate handling and one real conflict policy.
  8. Test platform lifecycle and background constraints.
  9. 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.