Offline-First Mobil Mimari: Isar, SQLite ve REST API ile Kusursuz Senkronizasyon
Günümüz mobil uygulama ekosisteminde kullanıcı deneyimini (UX) belirleyen en kritik unsurlardan biri, uygulamanın ağ bağlantısından bağımsız olarak ne kadar kararlı çalıştığıdır. Tünelde giden bir metroda, sinyalin zayıf olduğu kırsal bir bölgede ya da uçak modunda bile uygulamanızın çökmeden, takılmadan ve veri kaybetmeden çalışması gerekir.
Bu makalede, modern mobil uygulamalarda Offline-First (Önce Çevrimdışı) mimariyi kurmanın teorik temellerini, yerel veri tabanı (Local DB) seçim kriterlerini ve yerel verileri bir REST API ile kusursuz bir şekilde senkronize etmenin mühendislik pratiklerini ele alacağız.
1. Offline-First Mimari Nedir ve Neden Önemlidir?
Offline-First, uygulamanın birincil veri kaynağı (Single Source of Truth) olarak yerel veri tabanını (Local Database) kabul ettiği bir tasarım örüntüsüdür. Uygulama ağ isteklerinin sonucunu beklemek yerine, doğrudan cihaz üzerindeki yerel veriyi okur ve yazar. Arka planda çalışan bir Senkronizasyon Motoru (Sync Engine), bu yerel verileri uzak sunucuyla (Cloud/REST API) asenkron olarak eşitler.
Avantajları:
- Ultra Düşük Gecikme (Low Latency): UI, ağ gecikmesine (network latency) takılmadan yerel veri tabanından anında beslenir.
- Kesintisiz Çalışabilirlik: İnternet bağlantısı koptuğunda dahi tam fonksiyonellik sunulur.
- Daha Az Pil ve Veri Tüketimi: Ağ istekleri optimize edilerek paketler halinde (batching) gönderilir.
2. Mimari Tasarım Şeması
Kusursuz bir Offline-First mimari, katmanlı bir yapıda tasarlanmalıdır. İş mantığı (Business Logic), veri tabanı motorundan ve ağ katmanından tamamen izole edilmelidir.
+-------------------------------------------------------------+
| UI Katmanı |
+-------------------------------------------------------------+
│ (Reactive Streams / Streams)
▼
+-------------------------------------------------------------+
| Repository Katmanı |
| (Verinin nereden geleceğini yönetir) |
+-------------------------------------------------------------+
│ │
▼ (Okuma / Yazma) ▼ (Tetikleme)
+-------------------+ +-----------------+
| Local Data Source| | Sync Engine |
| (Isar / SQLite) | | (Senkronizasyon)|
+-------------------+ +-----------------+
│
▼ (REST API - JSON)
+-----------------+
|Remote Data Source|
+-----------------+
3. Yerel Veri Tabanı Seçimi: SQLite mı, Isar mı?
| Özellik | SQLite (Drift / Moor) | Isar (NoSQL) |
|---|---|---|
| Tür | İlişkisel (SQL) | Doküman / Nesne Tabanlı (NoSQL) |
| Hız | Orta / İyi | Çok Hızlı (Asenkron & Çoklu İzlek) |
| Reaktivite | Stream desteği var (Üçüncü parti kütüphanelerle) | Doğal reaktif (Query Watchers) |
| Şema Göçü (Migration) | Manuel SQL sorguları ile zorlayıcı | Otomatik şema yönetimi ile kolay |
Öneri: Eğer uygulamanız karmaşık çoklu tablolara ve ilişkisel ACID işlemlerine yoğun şekilde ihtiyaç duyuyorsa SQLite (Drift); performans, reaktivite, kolay geliştirme ve Flutter ile mükemmel uyum arıyorsanız modern Isar Database tercih edilmelidir.
4. Senkronizasyon Stratejileri ve Veri Modeli Tasarımı
Verilerin senkronize edilebilmesi için veri modellerimizde bazı meta-verilere (meta-fields) ihtiyacımız vardır. Her tablo/doküman şu alanları barındırmalıdır:
dart
abstract class SyncableEntity {
String get id; // Evrensel tekil kimlik (UUID v4)
DateTime get lastUpdatedAt; // Son güncellenme zamanı
bool get isDirty; // Sunucuya gönderilmeyi bekleyen yerel değişiklik var mı?
bool get isDeleted; // Soft-delete (Yumuşak silme) bayrağı
}
Kritik Tasarım Örüntüsü: Outbox (Giden Kutusu) Tasarımı
Kullanıcı çevrimdışıyken bir işlem gerçekleştirdiğinde, bu işlem doğrudan sunucuya gönderilemediği için bir Outbox tablosuna kuyruğa alınır.
dart
import 'package:isar/isar.dart';
part 'outbox_action.g.dart';
@collection
class OutboxAction {
Id? id = Isar.autoIncrement;
@Index(unique: true)
late String entityId; // Değişen kaydın ID'si
late String entityName; // 'Task', 'User', 'Order' vb.
@enumerated
late ActionType actionType; // CREATE, UPDATE, DELETE
late String payload; // Değişikliğin JSON hali
late DateTime createdAt;
}
enum ActionType { create, update, delete }
5. Senkronizasyon Algoritması (Sync Engine)
Senkronizasyon motoru iki yönlü çalışır: Upstream (Yerelden Sunucuya) ve Downstream (Sunucudan Yerelle).
Adım 1: Upstream (Yerel Değişikliklerin Sunucuya İletilmesi)
dart
class SyncEngine {
final Isar _localDb;
final ApiClient _apiClient;
SyncEngine(this._localDb, this._apiClient);
Future syncUpstream() async {
// 1. Bekleyen yerel işlemleri oku
final pendingActions = await _localDb.outboxActions
.where()
.sortByCreatedAt()
.findAll();
for (var action in pendingActions) {
try {
bool isSuccess = false;
switch (action.actionType) {
case ActionType.create:
isSuccess = await _apiClient.post(
'/api/${action.entityName.toLowerCase()}',
action.payload,
);
break;
case ActionType.update:
isSuccess = await _apiClient.put(
'/api/${action.entityName.toLowerCase()}/${action.entityId}',
action.payload,
);
break;
case ActionType.delete:
isSuccess = await _apiClient.delete(
'/api/${action.entityName.toLowerCase()}/${action.entityId}',
);
break;
}
if (isSuccess) {
// İşlem başarılı, kuyruktan sil
await _localDb.writeTxn(() async {
await _localDb.outboxActions.delete(action.id!);
});
}
} catch (e) {
// Hata durumunda loglama yap ve bir sonraki senkronizasyon döngüsünü bekle
print("Senkronizasyon hatası: $e");
break;
}
}
}
}
Adım 2: Downstream (Sunucudaki Değişikliklerin Yerelle Eşitlenmesi)
Çakışmaları önlemek için sunucu tarafındaki güncel veriler çekilirken en son başarılı senkronizasyon zaman damgası (lastSyncTimestamp) parametre olarak gönderilir.
dart
Future syncDownstream() async {
final lastSync = await _getLastSyncTimestamp();
// Sadece son senkronizasyondan sonra değişen verileri iste
final response = await _apiClient.get('/api/sync?since=${lastSync.toIso8601String()}');
if (response.statusCode == 200) {
final List remoteChanges = response.data['changes'];
await _localDb.writeTxn(() async {
for (var change in remoteChanges) {
// Çakışma Kontrolü: Eğer yerelde isDirty true ise yerel veriyi ezme (Conflict Resolution)
final localEntity = await _localDb.tasks.filter().idEqualTo(change['id']).findFirst();
if (localEntity == null || !localEntity.isDirty) {
// Yerelde değişiklik yoksa veya kayıt yeniyse güncelle
await _localDb.tasks.put(Task.fromJson(change));
}
}
});
await _saveLastSyncTimestamp(DateTime.now());
}
}
6. Çakışma Çözümleme (Conflict Resolution) Stratejileri
Çevrimdışı dünyada aynı veri hem sunucuda hem de yerelde güncellenmiş olabilir. Bu durumlar için şu stratejilerden biri seçilmelidir:
- Last-Write-Wins (LWW): Son güncelleyen kazanır. Zaman damgası (Timestamp) en büyük olan veri kabul edilir.
- Server-Wins: Sunucudaki veri her zaman doğru kabul edilir, yerel değişiklikler ezilir.
- Client-Wins: Kullanıcının cihazındaki değişiklik önceliklidir.
- Merge (Birleştirme): Çakışan alanlar analiz edilerek birleştirilir (Örn: Farklı alanlar güncellendiyse ikisi de korunur).
Sonuç
Offline-First mimari kurmak, ilk etapta karmaşık görünse de doğru veri tabanı seçimi (Isar/SQLite) ve sağlam bir Outbox Tasarımı ile uygulamanızın kalitesini bambaşka bir seviyeye taşır. Ağ hatalarını birer istisna olmaktan çıkarıp uygulamanızın doğal akışının bir parçası haline getirmek, kullanıcı sadakatini artırmanın en kesin yoludur.
Profesyonel Destek ve İletişim
Kurumsal mobil uygulamalarınızda yüksek performanslı veri tabanı mimarileri, senkronizasyon algoritmaları ve Offline-First stratejileri geliştirmek için bir uzmana mı ihtiyacınız var?
Ankara başta olmak üzere tüm dünya çapındaki projelerinize vizyon katmak ve mühendislik kalitesini artırmak için benimle iletişime geçebilirsiniz.
- Unvan: Nuh Mehmet Demirkol - Lisanslı Bilgisayar Mühendisi
- Lokasyon: Ankara, Türkiye
- Portfolyo ve Hizmetler: nmdemirkol.com
- Doğrudan İletişim (WhatsApp): +90 507 823 68 11
Geleceğin mobil mimarilerini bugünden inşa edelim.
Top comments (0)