Skip to content

Offline Setup

Offline Tier Configuration

Step-by-step guide for implementing each offline tier in your features.

Use the Offline Tiers Reference to determine the appropriate tier for your feature.

Quick decision guide:

  • Data is live/volatile -> Tier 0 (Online-only)
  • Data is reference/historical -> Tier 1 (Read cache fallback)
  • Users need to perform actions offline -> Tier 2 (Offline write queue)
  • App must work fully offline -> Tier 3 (Full sync)

Default behavior. Standard Convertor pipeline with no offline handling.

Ferry’s cache is enabled by default in the GQL client setup:

@riverpod
GqlClient gqlClient(Ref ref) {
final cache = Cache(); // Ferry's normalized cache
// ... link setup
return GqlClient(link: link, cache: cache);
}
@riverpod
Stream<bool> connectivity(Ref ref) {
return Connectivity().onConnectivityChanged.map(
(result) => result != ConnectivityResult.none,
);
}
Widget build(BuildContext context, WidgetRef ref) {
final isOnline = ref.watch(connectivityProvider).value ?? true;
final dataAsync = ref.watch(featureNotifierProvider);
return Column(
children: [
if (!isOnline)
const MaterialBanner(
content: Text('You are offline. Showing cached data.'),
actions: [SizedBox.shrink()],
),
Expanded(child: dataAsync.when(/* ... */)),
],
);
}
@freezed
class QueuedMutation with _$QueuedMutation {
const factory QueuedMutation({
required String id,
required String type,
required Map<String, dynamic> params,
required DateTime createdAt,
@Default(0) int retryCount,
}) = _QueuedMutation;
factory QueuedMutation.fromJson(Map<String, dynamic> json) =>
_$QueuedMutationFromJson(json);
}
@riverpod
class OfflineQueue extends _$OfflineQueue {
@override
Future<List<QueuedMutation>> build() async {
final cached = await ref.read(cachingServiceProvider)
.read<List<Map<String, dynamic>>>('offline_queue');
return cached?.map(QueuedMutation.fromJson).toList() ?? [];
}
Future<void> enqueue(QueuedMutation mutation) async {
final current = state.value ?? [];
final updated = [...current, mutation];
await _persist(updated);
state = AsyncData(updated);
}
Future<void> _persist(List<QueuedMutation> queue) async {
await ref.read(cachingServiceProvider).store(
key: 'offline_queue',
value: queue.map((m) => m.toJson()).toList(),
);
}
}
@riverpod
class OfflineSyncService extends _$OfflineSyncService {
@override
void build() {
// Listen for connectivity changes
ref.listen(connectivityProvider, (_, isOnline) {
if (isOnline.value == true) {
_syncQueue();
}
});
}
Future<void> _syncQueue() async {
final queue = await ref.read(offlineQueueProvider.future);
for (final mutation in queue) {
try {
await _executeMutation(mutation);
// Remove from queue on success
} catch (e) {
break; // Stop on first failure
}
}
}
}

Tier 3 requires significant additional infrastructure beyond the scope of this quick-start guide. Key components needed:

  1. Local database (Drift, Isar, or Hive)
  2. Sync engine with version tracking
  3. Conflict resolution strategy per entity
  4. Background sync service

See the Offline Tiers Reference for architectural guidance on Tier 3 implementation.