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

Flutter’da Offline-First Yaklaşımı: Yerel Veri Yönetimi

İçindekiler

Modern mobil uygulamalarda çevrimdışı çalışabilme yeteneği artık bir lüks değil, temel bir gereklilik. Chipode olarak Sudoku uygulamamızda, kullanıcılarımıza kesintisiz bir deneyim sunmak için offline-first yaklaşımını benimsedik. Bu yazıda, bu yaklaşımı nasıl implemente ettiğimizi detaylıca anlatacağız.

Offline-First Nedir?

Offline-first, uygulamanın internet bağlantısı olmadan da tam fonksiyonel çalışabilmesini hedefleyen bir tasarım yaklaşımıdır. Bu yaklaşımın temel prensipleri:

1. Yerel veri önceliği
2. Asenkron senkronizasyon
3. Çakışma çözümleme
4. Düşük bant genişliği optimizasyonu
5. Progressive enhancement

Veri Depolama Stratejisi

1- Local Storage Provider

				
					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(),
    ));
  }
  
  // Diğer metodlar...
}


				
			

2- Model Tanımlamaları

				
					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 ve metodlar...
}

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

  @HiveField(1)
  final DateTime lastModified;

  @HiveField(2)
  final DateTime? lastSyncAttempt;

  // Constructor ve metodlar...
}


				
			

Senkronizasyon Mekanizması

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 {
      // Senkronize edilmemiş oyunları al
      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;
    }
  }
}



				
			

Çakışma Çözümleme

1- Merge Strategy

				
					dart
class GameStateMergeStrategy {
  GameState merge(GameState local, GameState remote) {
    // Zaman damgası kontrolü
    if (local.lastModified.isAfter(remote.lastModified)) {
      return local;
    }
    
    // Skor karşılaştırması
    if (local.score > remote.score) {
      return local.copyWith(
        synced: true,
        lastModified: DateTime.now(),
      );
    }
    
    return remote;
  }
}



				
			

UI Entegrasyonu

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(
                  'Çevrimdışı mod - Değişiklikleriniz bağlantı kurulduğunda senkronize edilecek',
                  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 Optimizasyonu

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 Stratejisi

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);
  });
}

				
			

Sonuç

Offline-first yaklaşımı ile Chipode Sudoku uygulaması:

1. İnternet bağlantısından bağımsız çalışabilir
2. Veri kaybı riski minimize edilmiştir
3. Kullanıcı deneyimi kesintisizdir
4. Bant genişliği kullanımı optimize edilmiştir
5. Senkronizasyon şeffaf ve güvenilirdir

İleriye Dönük Planlar

– Çoklu cihaz senkronizasyonu
– Differential sync implementasyonu
– Offline analytics entegrasyonu
– Push notification desteği

Kaynaklar

Twitter
LinkedIn
Diğer Yazılar
İletişim

Chipode Uygulamaları

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