Offline Tiers Reference
Offline Capability Tiers
Each feature declares its offline tier. Most features need only Tier 0 or 1.
Offline Tiers Reference
Section titled “Offline Tiers Reference”Overview
Section titled “Overview”MOFA’s offline framework defines four tiers of offline capability. Each feature explicitly declares its tier, and the implementation complexity increases with each level.
Tier 0: Online-Only
Section titled “Tier 0: Online-Only”Behavior
Section titled “Behavior”- No offline support
- Shows error/loading state when network is unavailable
- Standard behavior - no additional implementation needed
When to Use
Section titled “When to Use”- Highly volatile data (live polls, real-time dashboards)
- Features that are meaningless without server connectivity
- Admin/management features used in reliable network environments
Implementation
Section titled “Implementation”Default behavior. No additional code required.
@riverpodclass LivePollNotifier extends _$LivePollNotifier { @override Stream<PollResults> build(String pollId) { // Standard implementation - no offline handling final repository = ref.watch(pollRepositoryProvider); return repository.subscribeToPoll(pollId: pollId); }}Tier 1: Read Cache Fallback
Section titled “Tier 1: Read Cache Fallback”Behavior
Section titled “Behavior”- Serves last successfully fetched data from Ferry’s cache when offline
- Shows a “stale data” or “offline” indicator to the user
- New data fetched automatically when connectivity returns
When to Use
Section titled “When to Use”- Reference data (event schedule, attendee list, venue maps)
- History data (notification history, past bookings)
- Any feature where showing recent data is better than showing nothing
Implementation
Section titled “Implementation”Ferry provides Tier 1 automatically via its built-in cache. Add connectivity awareness:
@riverpodclass EventScheduleNotifier extends _$EventScheduleNotifier { @override Stream<List<EventModel>> build() { final repository = ref.watch(eventRepositoryProvider); final connectivity = ref.watch(connectivityProvider);
return repository.queryList( filter: EventFilterCriteria(conferenceId: conferenceId), sort: EventSortCriteria(startTime: SortOptions.ascending), ); // Ferry's cache serves stale data when offline automatically }}
// UI shows offline indicator based on connectivity stateWidget build(BuildContext context, WidgetRef ref) { final isOnline = ref.watch(connectivityProvider); final eventsAsync = ref.watch(eventScheduleNotifierProvider);
return Column( children: [ if (!isOnline) const OfflineBanner(), Expanded(child: eventsAsync.when(/* ... */)), ], );}Key Points
Section titled “Key Points”- Ferry’s normalized cache (
__typename:id) serves entities individually fetchPolicy: FetchPolicy.CacheAndNetworkfetches from cache first, then network- No additional storage layer needed
Tier 2: Offline Write Queue
Section titled “Tier 2: Offline Write Queue”Behavior
Section titled “Behavior”- Tier 1 capabilities (read cache fallback)
- Mutations queued locally when offline
- Queued mutations synced automatically when connectivity returns
- Optimistic UI updates for queued mutations
When to Use
Section titled “When to Use”- User actions that should persist (mark attendance, submit feedback)
- Forms that can be submitted later
- Toggle actions (bookmark, like, RSVP)
Implementation
Section titled “Implementation”Requires a caching service queue and sync coordinator:
// Offline mutation queue@riverpodclass OfflineMutationQueue extends _$OfflineMutationQueue { @override List<QueuedMutation> build() => [];
Future<void> enqueue(QueuedMutation mutation) async { // Store in local cache await ref.read(cachingServiceProvider).store( key: 'offline_queue', value: [...state, mutation], ); state = [...state, mutation]; }
Future<void> sync() async { final connectivity = ref.read(connectivityProvider); if (!connectivity) return;
for (final mutation in state) { try { await _executeMutation(mutation); state = state.where((m) => m.id != mutation.id).toList(); } catch (e) { // Keep in queue for retry break; // Stop on first failure to maintain order } } }}
// Feature repository with offline supportclass AttendanceRepository { Future<void> markAttendance({ required String attendeeId, bool optimistic = true, }) async { final isOnline = ref.read(connectivityProvider); if (isOnline) { // Direct mutation await upsertDatasource.execute((params, optimistic)); } else { // Queue for later await ref.read(offlineMutationQueueProvider.notifier).enqueue( QueuedMutation( type: 'markAttendance', params: {'attendeeId': attendeeId}, createdAt: DateTime.now(), ), ); } }}Key Points
Section titled “Key Points”- Queue persists in local cache (survives app restart)
- Mutations replayed in order
- Failed mutations remain in queue for retry
- UI shows queued/pending state for offline mutations
Tier 3: Full Sync
Section titled “Tier 3: Full Sync”Behavior
Section titled “Behavior”- Tier 2 capabilities (read cache + write queue)
- Local database mirrors server state
- Bidirectional sync with conflict resolution
- App functions fully offline for extended periods
When to Use
Section titled “When to Use”- Field work applications
- Offline-first experiences
- Scenarios requiring extended offline operation
Implementation
Section titled “Implementation”Requires local database, sync engine, and conflict resolution:
// Sync engine (simplified)@riverpodclass SyncEngine extends _$SyncEngine { @override SyncState build() => SyncState.idle;
Future<void> fullSync() async { state = SyncState.syncing;
// 1. Push local changes await _pushLocalChanges();
// 2. Pull remote changes await _pullRemoteChanges();
// 3. Resolve conflicts await _resolveConflicts();
state = SyncState.idle; }}
// Conflict resolution strategiesenum ConflictStrategy { serverWins, // Server data always takes precedence clientWins, // Client data always takes precedence latestWins, // Most recent timestamp wins manual, // User resolves conflict manually}Key Points
Section titled “Key Points”- Requires version tracking (timestamps or vector clocks)
- Conflict resolution strategy must be defined per entity
- Local database schema must match domain models
- Consider data volume and sync frequency
Tier Decision Matrix
Section titled “Tier Decision Matrix”| Factor | Tier 0 | Tier 1 | Tier 2 | Tier 3 |
|---|---|---|---|---|
| Read offline | No | Yes (cached) | Yes (cached) | Yes (local DB) |
| Write offline | No | No | Yes (queued) | Yes (synced) |
| Implementation effort | None | Minimal | Medium | High |
| Storage requirement | None | Ferry cache | Cache + queue | Local database |
| Conflict handling | N/A | N/A | Retry on sync | Full resolution |
| Typical features | Live data | Reference data | User actions | Offline-first |
Integration with Caching Service
Section titled “Integration with Caching Service”The caching service provides the foundation for Tier 1-3:
- Secure Cache: Tokens and credentials (available offline)
- Simple Cache: Preferences and settings (Tier 1+)
- Complex Cache: Offline queue and sync metadata (Tier 2+)
See ADR-007: Offline Tiers for the full decision record.