Caching, offline and multiple sources
Offline support is not a class but a sequence of decisions, all made in the repository. The official offline-first page describes three read strategies and two write strategies.
Read strategies
1. Local as fallback (Future-based) — try the server first, fall back to the local database on failure. Simple, but the user waits for the network every time.
2. `Stream`-based — the page treats this as optimal: the repository emits twice, first the fast local data, then the refreshed data from the server. The user sees something immediately.
3. Local-only — reads always come from the database, with server synchronisation handled by a separate sync() method. The most predictable option, but it requires sync logic.
Write strategies
1. Online-only — API first, database on success. Data is always in sync, but offline writes are impossible. 2. Offline-first — database first, then the API. The user can work offline; in exchange you accept the risk the page states explicitly: if the network call fails, the local database and the server fall out of sync.
The fix for that last case is a `synchronized` flag: every written record is stored with synchronized: false, and a background process later sends only those records.
class ProfileRepositoryOfflineFirst implements ProfileRepository {
ProfileRepositoryOfflineFirst({
required ApiClientService api,
required DatabaseService database,
}) : _api = api,
_database = database;
final ApiClientService _api; // remote service
final DatabaseService _database; // lokal service
// ── OXUMA: Stream əsaslı (rəsmi sənədin optimal saydığı variant) ──
@override
Stream<UserProfile> watchProfile() async* {
// 1) Lokal məlumat dərhal verilir — ekran boş qalmır.
final local = await _database.fetchUserProfile();
if (local != null) yield local;
// 2) Sonra serverdən yenilənmiş məlumat.
try {
final remote = await _api.getUserProfile();
await _database.updateUserProfile(remote);
yield remote;
} catch (_) {
// Şəbəkə yoxdur: lokal məlumat artıq verilib, xəta udulur.
// Lokal da yoxdursa, çağıran tərəf boş axın alır və bunu idarə edir.
if (local == null) throw const NetworkFailure();
}
}
// ── YAZMA: offline-first (əvvəlcə lokal, sonra server) ──
@override
Future<void> updateProfile(UserProfile profile) async {
// 1) Lokal yazı dərhal baş verir → UI cavab verir, offline işləyir.
// Qeyd sinxron olmayan kimi işarələnir.
await _database.updateUserProfile(
profile.copyWith(synchronized: false),
);
// 2) Serverə göndərməyə cəhd.
try {
await _api.putUserProfile(profile);
// Uğurlu: bayraq qaldırılır.
await _database.updateUserProfile(
profile.copyWith(synchronized: true),
);
} catch (_) {
// Uğursuz: qeyd `synchronized: false` qalır və sonra göndəriləcək.
// Bu, sənədin açıq qeyd etdiyi "müvəqqəti desinxronizasiya"dır.
}
}
// ── SİNXRONİZASİYA: yalnız göndərilməmiş qeydlər ──
@override
Future<void> sync() async {
final pending = await _database.fetchUnsynchronizedProfiles();
for (final profile in pending) {
try {
await _api.putUserProfile(profile);
await _database.updateUserProfile(
profile.copyWith(synchronized: true),
);
} catch (_) {
// Bu qeyd növbəti dəfəyə qalır; digərlərini dayandırmır.
}
}
}
}`Stream`-based reads and offline-first writes. Note how two services (local and remote) are orchestrated by one repository.
| Strategy | What the user sees | Its price | When to choose it |
|---|---|---|---|
| Read: local fallback (`Future`) | Waits for the network; stale data on failure | Simplest; a wait on every open | The data must be strictly fresh |
| Read: `Stream` (local → remote) | Data immediately, then a silent refresh | Two emissions mean the UI builds twice | Most list and detail screens — the option the official page treats as optimal |
| Read: local-only plus `sync()` | Always immediate; freshness depends on the sync | Needs sync logic and conflict resolution | Fully offline-capable apps (notes, field work) |
| Write: online-only | Cannot write offline | Simple; no desync risk | Payments, order confirmation — the server has the final word |
| Write: offline-first plus `synchronized` | Instant response, works offline | Temporary desync plus sync code | Notes, favourites, settings — low conflict risk |
Who knows what. The division of responsibility does not change in this setup:
DatabaseService— SQL or key-value operations. The official docs provide two separate design patterns for this: key-value for small data, SQL for lists.ApiClientService— the REST calls.- The repository — which source to read from, when to write, when to synchronise. The whole strategy lives here.
- The UI — nothing. The screen subscribes to a
Streamand has no idea where the data came from.
What triggers a sync. The official page mentions a Timer or background processes (packages such as workmanager). In practice three triggers are used together: app start, connectivity returning (ConnectivityService), and an explicit user refresh.
Conflicts. When both sides modify the same record, who wins must be a deliberate choice: the server wins (simple), the local copy wins (risky), or last-write-by-timestamp. That decision also lives in the repository, and it needs a test — otherwise you get "my data disappeared" bugs.
The most common mistake: trying to build offline support in the UI — writing if (hasConnection) … else … on every screen. Each screen then carries its own strategy and behaviour diverges between them.
The right approach: the UI does not know the connectivity status. It only renders the data and, if needed, an "unsynchronised" badge.
Practice. Convert one feature to offline-first: Stream-based reads plus writes with a synchronized flag. Then write two tests: (1) with no network, a read returns the local data; (2) when a write fails, the record stays synchronized: false and sync() sends it later.
Done means: in airplane mode the app shows data and allows writing, and after connectivity returns sync() pushes the record.
📚 Sources and documentation
- Offline-first supportofficialdocs.flutter.dev
The source for this topic: three read strategies, two write strategies and the `synchronized` flag.
- SQL storage patternofficialdocs.flutter.dev
Building the local service for lists and relational data.
- Key-value data patternofficialdocs.flutter.dev
For settings and small data — at the `shared_preferences` level.
- Optimistic state patternofficialdocs.flutter.dev
Updating the UI without waiting for a write's result — used together with offline-first.