Skip to content

Part 5: Background Sync Service

Your Linkbox pattern is strong: sync immediately, then periodic timer, pause on background/offline, resume and sync once.

Sync lifecycle

Background Sync Service

dart
class BackgroundSyncService {
  final ExpenseSyncService syncService;

  Timer? _timer;
  String? _currentUserId;
  bool _paused = false;

  static const _interval = Duration(seconds: 30);

  BackgroundSyncService({required this.syncService});

  Future<void> startSyncFor(String userId) async {
    if (_currentUserId == userId && _timer != null) return;

    _currentUserId = userId;
    _paused = false;

    await syncOnce();

    _timer = Timer.periodic(_interval, (_) {
      unawaited(syncOnce());
    });
  }

  Future<void> syncOnce() async {
    if (_paused || _currentUserId == null) return;

    await syncService.pushUnsynced(_currentUserId!);
    await syncService.pullDelta(_currentUserId!);
  }

  void pause() {
    _paused = true;
    _timer?.cancel();
    _timer = null;
  }

  Future<void> resume() async {
    if (_currentUserId == null) return;
    _paused = false;
    await syncOnce();
    _timer ??= Timer.periodic(_interval, (_) => unawaited(syncOnce()));
  }

  void stop() {
    _timer?.cancel();
    _timer = null;
    _currentUserId = null;
    _paused = false;
  }
}

Network-aware Orchestration (Riverpod)

dart
final networkSyncOrchestratorProvider = Provider<void>((ref) {
  ref.listen<AsyncValue<NetworkUiState>>(networkStatusStreamProvider, (prev, next) async {
    if (!next.hasValue) return;

    final userId = ref.read(authProvider).currentUser?.uid;
    if (userId == null) return;

    final sync = await ref.read(backgroundSyncServiceProvider.future);
    final state = next.value!;

    if (state.state == NetworkState.offline) {
      sync.pause();
    } else if (state.state == NetworkState.online) {
      await sync.resume();
    }
  });
});

Previous: Part 4 - Riverpod State Management

Next: Part 6 - Integration and Production TODO