flutterda-offline-first-yaklasimi-yerel-veri-yonetimi

Offline-First Approach in Flutter: Local Data Management

Table of Contents

In modern mobile apps, the ability to work offline is no longer a luxury, but a basic necessity. As Chipode, we adopted the offline-first approach in our Sudoku app to provide a seamless experience for our users. In this article, we will explain in detail how we implemented this approach.

What is Offline-First?

Offline-first is a design approach that aims to make the application fully functional without an internet connection. The basic principles of this approach:

1. Local data priority
2. Asynchronous synchronization
3. Conflict resolution
4. Low bandwidth optimization
5. Progressive enhancement

Data Storage Strategy

1- Yerel Depolama Sağlayıcısı

				
					dart
abstract class LocalStorageProvider {
  Future<void> saveGame(GameState state);
  Future<GameState?> loadGame(String gameId);
  Future<List<GameState>> getUnsynced();
  Future<void> markSynced(String gameId);
}

class HiveStorageProvider implements LocalStorageProvider {
  late Box<GameState> _gameBox;
  late Box<SyncStatus> _syncBox;
  
  Future<void> initialize() async {
    Hive.registerAdapter(GameStateAdapter());
    Hive.registerAdapter(SyncStatusAdapter());
    
    _gameBox = await Hive.openBox<GameState>('games');
    _syncBox = await Hive.openBox<SyncStatus>('sync_status');
  }
  
  @override
  Future<void> saveGame(GameState state) async {
    await _gameBox.put(state.id, state);
    await _syncBox.put(state.id, SyncStatus(
      synced: false,
      lastModified: DateTime.now(),
    ));
  }
  
  // Other methods...
}


				
			

2- Model Definitions

				
					dart
@HiveType(typeId: 0)
class GameState extends HiveObject {
  @HiveField(0)
  final String id;

  @HiveField(1)
  final List<List<int>> board;

  @HiveField(2)
  final int score;

  @HiveField(3)
  final DateTime lastPlayed;

  @HiveField(4)
  final GameDifficulty difficulty;

  // Constructor and methods...
}

@HiveType(typeId: 1)
class SyncStatus extends HiveObject {
  @HiveField(0)
  final bool synced;

  @HiveField(1)
  final DateTime lastModified;

  @HiveField(2)
  final DateTime? lastSyncAttempt;

  // Constructors ve methods...
}


				
			

Synchronization Mechanism

1- Sync Service

				
					dart
class SyncService {
  final LocalStorageProvider _storage;
  final GameRepository _repository;
  final ConnectivityService _connectivity;
  
  Stream<SyncState> syncGames() async* {
    // Bağlantı kontrolü
    if (!await _connectivity.isConnected) {
      yield SyncState.offline();
      return;
    }
    
    try {
      // Get unsynchronized games
      final unsynced = await _storage.getUnsynced();
      
      for (var game in unsynced) {
        yield SyncState.syncing(game.id);
        
        // Backend'e gönder
        await _repository.saveGame(game);
        
        // Senkronize edildi olarak işaretle
        await _storage.markSynced(game.id);
        
        yield SyncState.success(game.id);
      }
    } catch (e) {
      yield SyncState.error(e.toString());
    }
  }
}

				
			

2- Connectivity Service

				
					dart
class ConnectivityService {
  final _connectivity = Connectivity();
  final _controller = StreamController<ConnectivityStatus>();
  
  Stream<ConnectivityStatus> get status => _controller.stream;
  
  ConnectivityService() {
    _connectivity.onConnectivityChanged.listen((status) {
      _controller.add(_getStatus(status));
    });
  }
  
  Future<bool> get isConnected async {
    final status = await _connectivity.checkConnectivity();
    return status != ConnectivityResult.none;
  }
  
  ConnectivityStatus _getStatus(ConnectivityResult result) {
    switch (result) {
      case ConnectivityResult.mobile:
        return ConnectivityStatus.mobile;
      case ConnectivityResult.wifi:
        return ConnectivityStatus.wifi;
      case ConnectivityResult.none:
        return ConnectivityStatus.offline;
      default:
        return ConnectivityStatus.offline;
    }
  }
}



				
			

Conflict Resolution

1- Merge Strategy

				
					dart
class GameStateMergeStrategy {
  GameState merge(GameState local, GameState remote) {
    // Timestamp check
    if (local.lastModified.isAfter(remote.lastModified)) {
      return local;
    }
    
    // Score comparison
    if (local.score > remote.score) {
      return local.copyWith(
        synced: true,
        lastModified: DateTime.now(),
      );
    }
    
    return remote;
  }
}



				
			

UI Integration

1- Offline Indicator Widget

				
					dart
class OfflineIndicator extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<ConnectivityService>(
      builder: (context, service, child) {
        return StreamBuilder<ConnectivityStatus>(
          stream: service.status,
          builder: (context, snapshot) {
            if (snapshot.data == ConnectivityStatus.offline) {
              return Container(
                color: Colors.red.shade100,
                padding: EdgeInsets.symmetric(vertical: 8),
                child: Text(
                  'Offline mode - Your changes will be synchronized when a connection is established',
                  textAlign: TextAlign.center,
                ),
              );
            }
            return SizedBox.shrink();
          },
        );
      },
    );
  }
}

				
			

2- Game Screen

				
					dart
class GameScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          OfflineIndicator(),
          Expanded(
            child: Consumer<GameStateNotifier>(
              builder: (context, notifier, child) {
                return SudokuBoard(
                  state: notifier.state,
                  onCellUpdated: (row, col, value) {
                    notifier.updateCell(row, col, value);
                    // Otomatik kayıt
                    context.read<LocalStorageProvider>().saveGame(
                      notifier.state,
                    );
                  },
                );
              },
            ),
          ),
          SyncStatusIndicator(),
        ],
      ),
    );
  }
}


				
			

Performance Optimization

1- Batch Processing

				
					dart
class BatchSyncProcessor {
  static const int BATCH_SIZE = 10;
  
  Future<void> processBatch(List<GameState> states) async {
    final batches = states.chunked(BATCH_SIZE);
    
    for (var batch in batches) {
      await Future.wait(
        batch.map((state) => _syncSingle(state)),
      );
    }
  }
  
  Future<void> _syncSingle(GameState state) async {
    // Tek oyun senkronizasyonu
  }
}



				
			

2- Compression

				
					dart
extension GameStateCompression on GameState {
  String toCompressedString() {
    final bytes = utf8.encode(jsonEncode(toJson()));
    return base64Encode(gzip.encode(bytes));
  }
  
  static GameState fromCompressedString(String compressed) {
    final bytes = gzip.decode(base64Decode(compressed));
    final json = jsonDecode(utf8.decode(bytes));
    return GameState.fromJson(json);
  }
}

				
			

Test Strategy

1- Unit Tests

				
					dart
void main() {
  group('Offline Storage Tests', () {
    late LocalStorageProvider storage;
    
    setUp(() async {
      storage = HiveStorageProvider();
      await storage.initialize();
    });
    
    test('should save and load game state', () async {
      final state = GameState(
        id: 'test_game',
        board: List.generate(9, (_) => List.filled(9, 0)),
        score: 100,
      );
      
      await storage.saveGame(state);
      final loaded = await storage.loadGame('test_game');
      
      expect(loaded, isNotNull);
      expect(loaded?.score, equals(100));
    });
    
    test('should handle offline sync queue', () async {
      // Test implementation
    });
  });
}

				
			

1- Integration Tests

				
					dart
void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('should work offline and sync when online',
      (WidgetTester tester) async {
    await tester.pumpWidget(MyApp());

    // Çevrimdışı moda geç
    await tester.binding.setNetworkEnabled(false);

    // Oyunu oyna
    await tester.tap(find.byType(SudokuCell).first);
    await tester.enterText(find.byType(NumberPad), '5');
    await tester.pump();

    // Veri kaydedildi mi kontrol et
    final storage = HiveStorageProvider();
    final saved = await storage.loadGame('current_game');
    expect(saved, isNotNull);

    // Çevrimiçi moda geç
    await tester.binding.setNetworkEnabled(true);
    await tester.pump(Duration(seconds: 1));

    // Senkronizasyon kontrolü
    expect(find.byType(SyncStatusIndicator), findsOneWidget);
  });
}

				
			

Results

Chipode Sudoku implementation with offline-first approach:

1. Can work independently of an Internet connection
2. Risk of data loss is minimized
3. The user experience is seamless
4. Bandwidth utilization is optimized
5. Synchronization is transparent and reliable

Future Plans

– Multi-device synchronization
– Differential sync implementation
– Offline analytics integration
– Push notification support

References

Twitter
LinkedIn
Related Articles
Contact Form

Chipode Apps

loading...
Google Play Store
loading...
Apple Store