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 saveGame(GameState state);
Future loadGame(String gameId);
Future> getUnsynced();
Future markSynced(String gameId);
}
class HiveStorageProvider implements LocalStorageProvider {
late Box _gameBox;
late Box _syncBox;
Future initialize() async {
Hive.registerAdapter(GameStateAdapter());
Hive.registerAdapter(SyncStatusAdapter());
_gameBox = await Hive.openBox('games');
_syncBox = await Hive.openBox('sync_status');
}
@override
Future 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> 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 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();
Stream get status => _controller.stream;
ConnectivityService() {
_connectivity.onConnectivityChanged.listen((status) {
_controller.add(_getStatus(status));
});
}
Future 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(
builder: (context, service, child) {
return StreamBuilder(
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(
builder: (context, notifier, child) {
return SudokuBoard(
state: notifier.state,
onCellUpdated: (row, col, value) {
notifier.updateCell(row, col, value);
// Otomatik kayıt
context.read().saveGame(
notifier.state,
);
},
);
},
),
),
SyncStatusIndicator(),
],
),
);
}
}
Performance Optimizasyonu
1- Batch Processing
dart
class BatchSyncProcessor {
static const int BATCH_SIZE = 10;
Future processBatch(List states) async {
final batches = states.chunked(BATCH_SIZE);
for (var batch in batches) {
await Future.wait(
batch.map((state) => _syncSingle(state)),
);
}
}
Future _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





