
Teknik not
Flutter’da Offline-First Mimari: İnternet Olmadan da Çalışan Uygulama Nasıl Yapılır?

Mobil uygulamanız asansörde, metroda ya da zayıf mobil bağlantıda kullanılamaz hâle geliyorsa sorun yalnızca ağda değildir; uygulamanın veriyi ele alış biçimindedir. Flutter offline-first mimari, internet bağlantısını uygulamanın çalışması için bir ön koşul olmaktan çıkarır. Kullanıcı veriyi yerel kaynaktan görür, yaptığı değişiklikler cihazda güvenle saklanır ve bağlantı geri geldiğinde sunucuyla eşitlenir.
Bu yaklaşım yalnızca “veriyi önbelleğe almak” değildir. Okuma, yazma, hata yönetimi, senkronizasyon ve çakışma çözümü birlikte tasarlanmalıdır. Bu rehberde sürdürülebilir bir offline-first yapının temelini, örnek Dart kodlarıyla adım adım kuracağız.
Offline-first ne demek?
Offline-first bir uygulama, temel işlevlerinin çoğunu internet bağlantısı olmadan da sunabilir. Ağ erişimi varsa güncel veriyi alır; yoksa cihazdaki veriyi kullanır. Kullanıcı çevrimdışıyken yaptığı işlemler kaybolmaz, daha sonra gönderilmek üzere kuyruğa alınır.
Bu modelde akış kabaca şöyledir:
- Arayüz veriyi doğrudan API’den değil, repository katmanından ister.
- Repository hızlıca yerel veriyi döndürür.
- Ağ uygunsa uzak kaynak arka planda kontrol edilir.
- Yeni veri yerel veritabanına yazılır.
- Arayüz güncellenmiş yerel kaynağı dinlemeye devam eder.
Flutter’ın resmi mimari rehberi de repository katmanını verinin tek doğruluk kaynağı olarak konumlandırır. Offline destekli uygulamalarda yerel ve uzak veri kaynaklarını birleştirme, yeniden deneme ve önbellekleme sorumluluğu bu katmandadır.
Cache-first ile offline-first aynı şey değildir
İki yaklaşım sıkça karıştırılır. Cache-first modelinde daha önce indirilmiş veri bağlantı yokken gösterilebilir. Ancak kullanıcı yeni bir kayıt oluşturmak ya da mevcut kaydı değiştirmek istediğinde işlem çoğu zaman başarısız olur.
Offline-first mimaride ise hem okuma hem yazma akışı çevrimdışı çalışacak şekilde tasarlanır:
- Okumalar yerel veritabanından yapılır.
- Yeni ve değiştirilen kayıtlar önce cihaza yazılır.
- Bekleyen işlemler kalıcı bir kuyrukta tutulur.
- Senkronizasyon başarısız olursa veri kaybolmadan yeniden denenir.
- Aynı kaydın iki yerde değişmesi için bir çakışma kuralı uygulanır.
Kısacası cache, offline-first mimarinin bir parçası olabilir; fakat tek başına yeterli değildir.
Önerilen katmanlar
Bakımı kolay bir Flutter projesinde veri akışını üç parçaya ayırabilirsiniz:
UI / ViewModel
↓
Repository (tek doğruluk kaynağı)
↙ ↘
Local DB Remote API
LocalDataSource, SQLite tabanlı bir çözümden veri okur ve yazar. RemoteDataSource, HTTP isteklerini yönetir. Repository ise hangi kaynağın ne zaman kullanılacağına karar verir. Arayüzün bu iki veri kaynağına doğrudan erişmemesi önemlidir; aksi hâlde senkronizasyon kuralları ekranlara dağılır.
Örnek bir not modeli oluşturalım:
class Note {
const Note({
required this.id,
required this.title,
required this.updatedAt,
this.isPendingSync = false,
});
final String id;
final String title;
final DateTime updatedAt;
final bool isPendingSync;
}
isPendingSync, kaydın henüz sunucuya ulaşmadığını görünür kılar. Daha kapsamlı projelerde ayrı bir senkronizasyon tablosu tutmak daha esnek olur.
Okuma stratejisi: Önce yerel veri, sonra yenileme
Kullanıcı uygulamayı açtığında boş ekran karşısında ağ yanıtını beklememelidir. Yerel veriyi hemen göstermek ve uzak kaynağı arka planda yenilemek daha akıcı bir deneyim sağlar.
class NotesRepository {
NotesRepository(this.local, this.remote);
final NotesLocalDataSource local;
final NotesRemoteDataSource remote;
Stream<List<Note>> watchNotes() => local.watchAll();
Future<void> refresh() async {
try {
final remoteNotes = await remote.fetchAll();
await local.replaceSyncedNotes(remoteNotes);
} on Exception {
// Yerel veri gösterilmeye devam eder.
// Hata gözlemlenebilirlik katmanına ayrıca raporlanabilir.
}
}
}
Arayüz watchNotes() akışını dinler. refresh() başarılı olduğunda yeni kayıtlar yerel veritabanına yazılır ve aynı akış arayüzü otomatik olarak günceller. Böylece ekranın ağ ve veritabanı için iki farklı durum yönetmesine gerek kalmaz.
Yazma stratejisi: Önce cihaza kaydet
Offline-first yazma işleminde kullanıcının değişikliği önce yerel veritabanına kaydedilir. Ardından gönderilecek işlem kuyruğa eklenir.
Future<void> saveNote(Note note) async {
final pendingNote = Note(
id: note.id,
title: note.title,
updatedAt: DateTime.now().toUtc(),
isPendingSync: true,
);
await local.transaction(() async {
await local.upsert(pendingNote);
await local.enqueue(
SyncOperation.upsertNote(
entityId: pendingNote.id,
createdAt: pendingNote.updatedAt,
),
);
});
}
Not ve kuyruk kaydının aynı transaction içinde yazılması kritik bir ayrıntıdır. Uygulama iki işlem arasında kapanırsa “cihazda değişmiş ama gönderilecekler listesine girmemiş” bir kayıt oluşmaz.
Sunucu tarafında da aynı isteğin birden fazla kez güvenle işlenebilmesi gerekir. Bunun için her işleme benzersiz bir operationId eklemek ve API’yi idempotent tasarlamak, tekrar denemelerde yinelenen kayıt riskini azaltır.
Senkronizasyon kuyruğu nasıl çalışır?
Senkronizasyon servisi bekleyen işlemleri oluşturulma sırasına göre alır. Başarılı işlemi kuyruktan çıkarır; geçici hata alan işlemi ise daha sonra yeniden dener.
Future<void> syncPendingOperations() async {
final operations = await local.pendingOperations();
for (final operation in operations) {
try {
await remote.execute(operation);
await local.markOperationCompleted(operation.id);
await local.markEntitySynced(operation.entityId);
} on TemporaryNetworkException {
await local.scheduleRetry(operation.id);
break;
} on PermanentApiException catch (error) {
await local.markOperationFailed(operation.id, error.message);
}
}
}
Her hata aynı biçimde ele alınmamalıdır. Zaman aşımı, DNS sorunu veya 5xx yanıtı geçici olabilir ve yeniden denenebilir. Yetki hatası ya da sunucunun doğrulama reddi ise kullanıcıya açıklanması gereken kalıcı bir sorundur.
Yeniden denemelerde artan bekleme süresi kullanın. Örneğin 2, 4, 8 ve 16 saniyelik gecikmeler; kısa süreli kesintilerde sunucuyu gereksiz isteklerle zorlamaz. Bir miktar rastgele sapma eklemek, çok sayıda cihazın aynı anda yeniden bağlandığı durumlarda istek yığılmasını azaltır.
Bağlantı durumuna neden tamamen güvenmemelisiniz?
connectivity_plus gibi paketler cihazın Wi-Fi ya da mobil ağa bağlı olup olmadığını bildirir. Fakat Wi-Fi bağlantısının bulunması internetin gerçekten erişilebilir olduğu anlamına gelmez. Otel giriş ekranı, DNS problemi, zaman aşımı veya sunucu kesintisi hâlâ isteği başarısız kılabilir.
Bu nedenle bağlantı bilgisini senkronizasyonu tetikleyen bir ipucu olarak kullanın; ağ isteğinin başarılı olacağına dair garanti olarak değil. Her HTTP isteğinde zaman aşımı ve hata yönetimi bulunmalı, başarısız işlem kuyrukta kalmalıdır.
Uygulama ön plana geldiğinde, kullanıcı elle yenilediğinde ve bağlantı türü değiştiğinde senkronizasyonu tetiklemek iyi bir başlangıçtır. İşletim sistemlerinin arka plan çalışma kısıtlamaları nedeniyle yalnızca sürekli çalışan bir dinleyiciye güvenmeyin.
Veri çakışmaları nasıl çözülür?
Aynı kayıt telefonda çevrimdışı değiştirilirken başka bir cihazda da güncellenebilir. Senkronizasyon sırasında hangi sürümün geçerli olacağı önceden tanımlanmalıdır.
- Son yazan kazanır: Uygulaması kolaydır; ancak cihaz saati yanlışsa veya önemli veri söz konusuysa kayıp yaşatabilir.
- Sunucu sürümü kazanır: Merkezi tutarlılık sağlar fakat kullanıcının çevrimdışı değişikliğini silebilir.
- Alan bazında birleştirme: Farklı alanlardaki değişiklikleri korur; daha fazla geliştirme ve test gerektirir.
- Kullanıcıya seçtirme: Not, belge ya da içerik üretimi gibi kaybın pahalı olduğu senaryolarda iki sürümü gösterir.
Basit yapılacaklar listesinde son yazan kazanır yeterli olabilir. Finansal kayıtlar veya ortak düzenlenen belgelerde ise sürüm numarası, sunucu zamanı ve açık bir birleştirme politikası kullanmak daha güvenlidir.
Kullanıcı arayüzünde senkronizasyon durumu
Offline-first deneyim hataları gizlemek anlamına gelmez. Kullanıcı yaptığı işlemin cihazda kayıtlı olduğunu ve henüz sunucuya ulaşmadığını anlayabilmelidir.
- Cihaza kaydedildi, senkronizasyon bekliyor
- Senkronizasyon sürüyor
- Güncel
- İşlem kullanıcı müdahalesi gerektiriyor
Tam ekran “internet yok” uyarısı yerine ilgili kaydın yanında küçük bir durum göstergesi çoğu zaman daha yararlıdır. Kullanıcı temel işine devam ederken sistemin durumunu da görür.
Test etmeniz gereken senaryolar
Offline-first akışlar yalnızca uçak modunu açıp kapatarak doğrulanamaz. En az şu senaryoları otomasyon veya kontrollü manuel testlerle sınayın:
- Uygulama ilk kez açıldığında bağlantı yok.
- Veri indirilirken bağlantı kesiliyor.
- Kullanıcı çevrimdışıyken art arda birden fazla kayıt değiştiriyor.
- Uygulama kuyruk yazıldıktan hemen sonra kapatılıyor.
- Aynı işlem sunucuya iki kez ulaşıyor.
- Kimlik doğrulama anahtarı senkronizasyon sırasında geçersizleşiyor.
- Yerel ve uzak kayıt aynı anda değişiyor.
- Uygulama uzun süre çevrimdışı kaldıktan sonra yeniden bağlanıyor.
Repository, yerel veri kaynağı ve uzak veri kaynağı ayrı tutulduğunda bu senaryoları sahte bağımlılıklarla test etmek kolaylaşır.
Uygulama kontrol listesi
- Her veri türü için repository sınırını belirleyin.
- Yerel veritabanını okuma işlemlerinin ana kaynağı yapın.
- Yazmaları transaction içinde yerel veriye ve kalıcı kuyruğa ekleyin.
- Ağ isteklerine zaman aşımı, hata sınıflandırması ve tekrar deneme ekleyin.
- API işlemlerini mümkün olduğunca idempotent tasarlayın.
- Çakışma politikasını ürün gereksinimine göre açıkça belirleyin.
- Bekleyen ve başarısız işlemleri kullanıcıya anlaşılır biçimde gösterin.
- Kesinti, kapanma ve tekrar bağlanma senaryolarını test edin.
Sonuç
Flutter offline-first mimari, yalnızca internet yokken eski veriyi göstermekten ibaret değildir. Sağlam bir çözüm; repository katmanı, yerel veritabanı, kalıcı işlem kuyruğu, kontrollü yeniden deneme ve açık bir çakışma politikası üzerine kurulur.
En küçük uygulanabilir adım, bir özelliğin okumalarını yerel veritabanından sunmak ve uzak veriyi arka planda yenilemektir. Ardından yazma kuyruğunu ekleyerek kullanıcı işlemlerini bağlantıdan bağımsız hâle getirebilirsiniz. Sonuç, zayıf ağ koşullarında bile hızlı ve güven veren bir mobil deneyimdir.
Flutter arayüzünüz Android’in sistem çubukları veya klavyesiyle çakışıyorsa Flutter’da Android 15/16 Edge-to-Edge Sorunu rehberine de göz atabilirsiniz.
2 yorum