Skip to content

Part 6: Integration and Production TODO

This is the final step where we connect DB, state, sync, and UI into one predictable local-first feature.

Wire Providers Together

dart
final isarProvider = Provider<Isar>((_) => isar);

final expenseRepositoryProvider = Provider<ExpenseRepository>((ref) {
  return IsarExpenseRepository(ref.read(isarProvider));
});

final expenseSyncServiceProvider = Provider<ExpenseSyncService>((ref) {
  return ExpenseSyncService(
    remote: ref.read(expenseRemoteDataSourceProvider),
    repo: ref.read(expenseRepositoryProvider),
    syncState: ref.read(syncStateManagerProvider),
  );
});

final backgroundSyncServiceProvider = FutureProvider<BackgroundSyncService>((ref) async {
  final service = BackgroundSyncService(syncService: ref.read(expenseSyncServiceProvider));
  return service;
});

Production-Grade Expense Tracker TODO (with code)

1) Add indexes for heavy filters

dart
@Index()
late DateTime spentAt;

@Index()
late String category;

2) Add retry with exponential backoff

dart
Future<T> withRetry<T>(Future<T> Function() action) async {
  var delay = const Duration(seconds: 1);
  for (var i = 0; i < 5; i++) {
    try {
      return await action();
    } catch (_) {
      if (i == 4) rethrow;
      await Future.delayed(delay);
      delay *= 2;
    }
  }
  throw Exception('unreachable');
}

3) Add conflict resolution policy

dart
ExpenseLocal resolveConflict(ExpenseLocal local, ExpenseDto remote) {
  return remote.updatedAt.isAfter(local.updatedAt)
      ? remote.toLocal()
      : local;
}

4) Add observability counters

dart
class SyncMetrics {
  int pushed = 0;
  int pulled = 0;
  int failed = 0;
}

5) Add stale-data banner in UI

dart
if (state.lastSyncAt != null && DateTime.now().difference(state.lastSyncAt!) > const Duration(minutes: 10)) {
  return const Text('Data may be stale. Pull to refresh when online.');
}

6) Add background app lifecycle hooks

dart
late final AppLifecycleListener lifecycle;

void initLifecycle() {
  lifecycle = AppLifecycleListener(
    onResume: () => unawaited(syncService.resume()),
    onPause: syncService.pause,
    onDetach: syncService.stop,
  );
}

Final Wrap-up

At this point you have:

  • Local-first CRUD (Isar)
  • Riverpod pagination state
  • Optimistic UI updates
  • Background sync with online/offline orchestration
  • A production TODO list that maps directly to launch quality

Previous: Part 5 - Background Sync Service

Back to Guide Hub