Flutter uygulamanda internet bağlantısı kesildiğinde kullanıcı "bağlantı yok" ekranıyla mı karşılaşıyor, yoksa uygulama kesintisiz çalışmaya devam mı ediyor? Offline-first mimari, veriyi önce cihazda tutup ağı arka plana atan bir tasarım kararıdır ve Flutter'da bunun en olgun aracı drift'tir — tip-güvenli sorgular, derleme zamanı şema doğrulaması ve migration desteğiyle. Bu rehberde drift ile gerçek bir yerel veritabanı kuracak, şema migration'ı yazacak, yazma kuyruğu (outbox) ve çakışma çözümü stratejilerini konuşacak, bağlantı durumunu izleyeceksin.
💡 Pro Tip: Offline-first'e "önce UI'yı çalıştır, veriyi sonradan bağla" diye başlama — önce yerel şemanı ve senkron kuyruğunu tasarla, UI'yı ona bağla. Tersi sırada gidersen migration ve çakışma çözümünü sonradan UI'ya zorla sıkıştırmak zorunda kalırsın.
İçindekiler
- Offline-First Ne Demek, "Cache" ile Farkı Nedir?
- Drift Kurulumu ve Tip-Güvenli Sorgular
- Yerel Şema ve Migration Stratejisi
- Outbox Pattern ile Yazma Kuyruğu
- Çakışma Çözümü: Last-Write-Wins vs Alan Bazlı Merge
- Bağlantı Durumu ve Arka Planda Senkron
- Test Edilebilirlik: Senkron Mantığını UI'dan Ayırmak
- Şifreleme ve KVKK Açısından Yerel Veri
- Uçtan Uca Akış: Bağlantısız Bir Kayıt Nasıl Senkronlanır
- SSS
- Flutter'da offline-first uygulama nasıl yazılır?
- Drift ile sqflite arasındaki fark nedir?
- Çakışan kayıtlar senkronizasyonda nasıl çözülür?
- Flutter'da yerel veritabanı şeması nasıl migrate edilir?
- Güncelleme (Eylül 2026)
- Sonuç
- Kaynaklar
Offline-First Ne Demek, "Cache" ile Farkı Nedir?
Offline desteği genelde "API yanıtını sakla, tekrar göster" olarak düşünmek kolaydır — bu bir cache stratejisidir ve tek yönlüdür: sunucudan cihaza. Offline-first ise çift yönlüdür: kullanıcı bağlantı yokken de yeni veri üretebilir (form doldurma, not ekleme, durum değiştirme) ve bu veri yerel veritabanına yazılır, bağlantı geri geldiğinde sunucuya senkronize edilir.
Bu ayrım mimariyi doğrudan etkiler:
- Cache mimarisi: Sunucu verisi = tek doğruluk kaynağı (source of truth); yerel depo yalnız bir kopya.
- Offline-first mimari: Yerel veritabanı = birincil doğruluk kaynağı; sunucu senkron hedefi. UI her zaman yerelden okur, asla doğrudan ağ isteğinin sonucunu beklemez.
Bu yüzden offline-first bir Flutter uygulamasında ilk karar "hangi HTTP client" değil, "hangi yerel veritabanı ve hangi şema" sorusudur.
Drift Kurulumu ve Tip-Güvenli Sorgular
drift, SQLite üzerine inşa edilmiş, kod üretimiyle çalışan bir Dart ORM'idir. Kurulum için gerekli paketler:
yaml
1dependencies:2 drift: ^2.26.03 drift_flutter:4 path_provider:5 6dev_dependencies:7 drift_dev:8 build_runner:Not: Bu yazıdaki örnekler drift 2.26.0 API'sine göre yazılmıştır.
Tablo tanımı, ham SQL yerine Dart sınıfıyla yapılır — bu, derleme zamanında tip güvenliği sağlar:
dart
1class TodoItems extends Table {2 IntColumn get id => integer().autoIncrement()();3 TextColumn get title => text()();4 BoolColumn get isSynced => boolean().withDefault(const Constant(false))();5 DateTimeColumn get updatedAt => dateTime().nullable()();6}dart run build_runner build çalıştırıldığında drift, bu tablo tanımından database.g.dart dosyasını üretir. Veritabanı sınıfın bu üretilen sınıfı genişletir:
dart
1@DriftDatabase(tables: [TodoItems])2class AppDatabase extends _$AppDatabase {3 AppDatabase() : super(_openConnection());4 5 @override6 int get schemaVersion => 1;7}Sorgular ham SQL string'i değil, Dart metot zinciriyle yazılır — bu sayede kolon adını yanlış yazarsan derleme hatası alırsın, runtime'da sessizce boş sonuç dönmez:
dart
1// Ekleme2await database.into(database.todoItems).insert(3 TodoItemsCompanion.insert(title: 'Rapor yaz', updatedAt: Value(DateTime.now())),4);5 6// Okuma7final items = await database.select(database.todoItems).get();Kavram | Ham SQL yaklaşımı | drift yaklaşımı |
|---|---|---|
Kolon adı hatası | Runtime'da sessiz/hata | Derleme zamanı hatası |
Şema değişikliği | Elle string güncelleme | Tip-güvenli migration API |
Sorgu sonucu | Map<String, dynamic> | Üretilmiş Dart sınıfı |
Test | Gerçek dosya veya mock | NativeDatabase bellek-içi örnek |
Yerel Şema ve Migration Stratejisi
Uygulaman canlıya çıktıktan sonra şema değişecek — yeni kolon, yeni tablo, kaldırılan alan. drift bunu schemaVersion ve migration getter'ı üzerinden yönetir:
dart
1@override2int get schemaVersion => 3;3 4@override5MigrationStrategy get migration => MigrationStrategy(6 onCreate: (Migrator m) async {7 await m.createAll();8 },9 onUpgrade: (Migrator m, int from, int to) async {10 if (from < 2) {11 await m.addColumn(todoItems, todoItems.isSynced);12 }13 if (from < 3) {14 await m.addColumn(todoItems, todoItems.updatedAt);15 }16 },17);Burada kritik nokta: onUpgrade içindeki kontroller kümülatif olmalı. Kullanıcı sürüm 1'den doğrudan sürüm 3'e güncelliyorsa, hem from < 2 hem from < 3 bloğu sırayla çalışmalı — aksi halde from < 2 bloğu atlanan kullanıcılarda isSynced kolonu hiç oluşmaz ve bu kolona dokunan her sorgu "no such column" hatasıyla çöker. Migration testini gerçek cihaz sürüm geçmişini simüle ederek yazmak, offline-first uygulamalarda en çok gözden kaçan adımdır çünkü geliştirici genelde temiz kurulumla test eder, yükseltmeyle değil.
Outbox Pattern ile Yazma Kuyruğu
Kullanıcı bağlantısız durumdayken bir kayıt oluşturduğunda, bu yazmanın sunucuya ne zaman ve nasıl gideceğini birinin takip etmesi gerekir. Bunun için yaygın kullanılan desen outbox pattern'dir — genel dağıtık sistemler literatüründen gelir, drift'e veya Flutter'a özgü değildir:
- Yazma işlemi doğrudan sunucuya gitmez; önce yerel bir
pending_operations(outbox) tablosuna kaydedilir, aynı transaction içinde asıl veriyle birlikte. - Bağlantı geri geldiğinde arka planda çalışan bir senkron işçisi outbox tablosunu sırayla okur, her kaydı sunucuya gönderir, başarılı olanı outbox'tan siler.
- Asıl veri tablosundaki
isSyncedbayrağı, outbox kaydı başarıyla temizlendiğindetrueolur.
dart
1class PendingOperations extends Table {2 IntColumn get id => integer().autoIncrement()();3 TextColumn get entityId => text()();4 TextColumn get operationType => text()(); // create / update / delete5 TextColumn get payloadJson => text()();6 DateTimeColumn get createdAt => dateTime()();7}Outbox'ın en önemli garantisi: asıl veri ile outbox kaydı aynı transaction içinde yazılır. Ayrı ayrı yazarsan, ikisi arasında uygulama çökerse ya veri var ama senkron kaydı yok (veri asla sunucuya gitmez) ya da tam tersi olur.
Çakışma Çözümü: Last-Write-Wins vs Alan Bazlı Merge
İki cihaz aynı kaydı bağlantısızken değiştirip senkron olduğunda, hangi değişiklik kazanacak? Bu sorunun tek doğru cevabı yok — uygulamanın veri modeline göre iki yaygın strateji var:
- Last-write-wins (LWW): Her kayıtta bir
updatedAtzaman damgası tutulur, sunucu çakışan iki yazımdan en yeni zaman damgalı olanı kazanan ilan eder. Uygulaması basittir ama diğer cihazın değişikliği sessizce kaybolur. - Alan bazlı (field-level) merge: Kaydın her alanı kendi zaman damgasıyla izlenir; çakışma anında yalnız gerçekten çakışan alanlar için karar verilir, geri kalan alanlar her iki taraftan da korunur. Uygulaması daha karmaşıktır ama veri kaybı riski daha düşüktür.
Pratik kural: kullanıcı tek bir formu dolduruyor ve nadiren çakışma oluyorsa LWW yeterlidir. Aynı kayıt üzerinde birden fazla kullanıcı/cihaz eşzamanlı çalışıyorsa (paylaşımlı liste, ortak not) alan bazlı merge'e yatırım yapmak gerekir — aksi halde kullanıcılar birbirinin işini "kaybettiğini" fark eder ve güven kaybı oluşur.
Bağlantı Durumu ve Arka Planda Senkron
Senkron işçisinin ne zaman çalışacağını bilmesi için bağlantı durumunu izlemen gerekir. Flutter ekosisteminde bunun için connectivity_plus kullanılır:
dart
1final result = await Connectivity().checkConnectivity();2// result: List<ConnectivityResult> — birden fazla arayüz aynı anda aktif olabilir3 4Connectivity().onConnectivityChanged.listen((result) {5 if (!result.contains(ConnectivityResult.none)) {6 syncWorker.trigger();7 }8});Bu makalenin yazıldığı tarihte (2025-03-25) güncel connectivity_plus sürümü 6.1.3'tür (7 Şubat 2025'te yayınlandı).
İki önemli davranış notu, paketin kendi dokümantasyonundan:
checkConnectivity()tekil bir sonuç değil, liste döner — cihaz aynı anda hem Wi-Fi hem mobil veri arayüzüne sahip olabilir.onConnectivityChangedyalnız farklı değerleri yayınlamalıdır; ama bağlantı türünün "var" olması internet erişiminin garantisi değildir (captive portal, kısıtlı ağ). Kritik senkron öncesi gerçek bir ping/health-check isteğiyle doğrulama eklemek daha güvenlidir.
Sinyal | Ne anlama gelir | Senkrona etkisi |
|---|---|---|
ConnectivityResult.wifi/.mobile | Ağ arayüzü aktif | Senkron denemesi tetiklenebilir |
ConnectivityResult.none | Hiç arayüz yok | Outbox biriktirmeye devam eder |
Arayüz var ama istek timeout | Captive portal / kısıtlı ağ | Senkron başarısız, tekrar dener |
Test Edilebilirlik: Senkron Mantığını UI'dan Ayırmak
Outbox işçisini ve çakışma çözücüyü widget ağacına gömersen, onları test etmek için widget test altyapısı kurmak zorunda kalırsın — yavaş ve kırılgan. Bunun yerine senkron mantığını sade bir Dart sınıfına (repository/service) taşı; widget yalnız bu sınıfı çağırsın. Böylece:
- Senkron işçisini bellek-içi bir veritabanı örneğiyle unit test edebilirsin, gerçek dosya sistemi veya widget test framework'ü gerekmez.
- Sahte (fake) bir HTTP client ile "sunucu 500 döndü, outbox kaydı silinmemeli" gibi hata senaryolarını UI'sız doğrulayabilirsin.
- Migration testlerini, farklı
schemaVersionkombinasyonlarıyla tekrarlanabilir biçimde çalıştırabilirsin.
Pratik ayrım: SyncService sınıfı AppDatabase ve bir ApiClient alır, ikisi de constructor injection ile verilir — testte gerçek ApiClient yerine sahte bir uygulama geçirirsin, AppDatabase için ise gerçek drift altyapısını (dosyaya değil belleğe yazan bir bağlantıyla) kullanırsın. Bu sayede test, gerçek SQL davranışını (kısıtlar, tip dönüşümleri) de kapsar — sahte bir "in-memory Map" ile taklit edilen veritabanı bu garantiyi vermez.
Şifreleme ve KVKK Açısından Yerel Veri
Offline-first mimaride veri artık yalnız sunucuda değil, kullanıcının cihazında da kalıcı olarak duruyor — bu, KVKK/GDPR açısından ek sorumluluk demektir:
- Kişisel veri minimizasyonu: Yerel veritabanına yalnız uygulamanın çalışması için gerekli alanları yaz; sunucudan gelen her alanı "belki lazım olur" diye kopyalama.
- Silme talebi (unutulma hakkı): Kullanıcı hesabını sildiğinde, sunucu tarafındaki silme kadar yerel veritabanındaki kopyanın da temizlenmesi gerekir — bu genelde uygulama çıkışında veya hesap silme akışında tetiklenen bir
deleteAll()/veritabanı dosyasını silme adımıyla sağlanır. - Cihaz kaybı senaryosu: Yerel veritabanı şifrelenmemişse, cihaz kaybolduğunda/çalındığında veri de sızabilir. Hassas veri (kimlik, ödeme, sağlık bilgisi) taşıyan tablolarda işletim sisteminin sağladığı disk şifrelemesine ek olarak alan bazlı şifreleme değerlendirilmelidir; drift'in resmi Encryption sayfası şifreli
NativeDatabasekurulumunu ve mevcut bir veritabanını şifreleme adımlarını anlatır (bkz. Kaynaklar).
Uçtan Uca Akış: Bağlantısız Bir Kayıt Nasıl Senkronlanır
Tipik bir sahne şöyle işler: kullanıcı asansörde veya metroda, bağlantı yokken bir form dolduruyor. UI hiçbir uyarı göstermeden kaydı kabul ediyor çünkü yazma işlemi doğrudan yerel veritabanına gidiyor, ağı beklemiyor. Kayıt isSynced: false ile ve aynı transaction'da outbox'a düşüyor.
Kullanıcı yer üstüne çıktığında connectivity_plus dinleyicisi tetikleniyor, senkron işçisi outbox'ı sırayla boşaltıyor. Eğer aynı kayıt bu sırada başka bir cihazdan da değiştirilmişse, çakışma çözücü (LWW veya alan bazlı merge, hangisi seçildiyse) devreye giriyor. Kullanıcı için görünen tek şey: uygulama hiç durmadı.
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.
Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
Okuyucu Ödülü
Bu makaledeki drift + outbox + çakışma çözümü mimarisini kendi projene uygularken atlamaman gereken kontrol listesini hazırladım. Her madde, yukarıdaki bölümlerden birine karşılık gelir ve production'a çıkmadan önce tek tek işaretlenmesi gereken kritik noktalardır.
SSS
Flutter'da offline-first uygulama nasıl yazılır?
Önce yerel şemanı drift ile tanımlarsın (tablo sınıfları + migration stratejisi), sonra UI'nı doğrudan bu yerel veritabanından okuyacak şekilde kurarsın — ağ isteği UI'yı bloklamaz. Yazmalar önce yerele gider, aynı transaction içinde outbox tablosuna düşer; bağlantı geri geldiğinde bir senkron servisi outbox'ı boşaltıp sunucuyla eşitler.
Drift ile sqflite arasındaki fark nedir?
sqflite, SQLite'a ham erişim veren düşük seviyeli bir pakettir — sorguları elle SQL string'i olarak yazarsın. drift bunun üzerine kod üretimi ekler: tabloları Dart sınıfı olarak tanımlarsın, sorgular derleme zamanında tip kontrolünden geçer ve database.g.dart otomatik üretilir. Küçük, tek tablo'luk bir önbellek için sqflite yeterli olabilir; şeması büyüyecek, migration geçmişi olacak bir uygulamada drift'in tip güvenliği hata oranını düşürür.
Çakışan kayıtlar senkronizasyonda nasıl çözülür?
İki yaygın strateji var: last-write-wins (en yeni updatedAt zaman damgalı kayıt kazanır, diğeri sessizce kaybolur) ve alan bazlı merge (her alan kendi zaman damgasıyla izlenir, yalnız gerçekten çakışan alanlar için karar verilir). Tek kullanıcı senaryosunda LWW yeterlidir; paylaşımlı/çok kullanıcılı kayıtlarda alan bazlı merge veri kaybını azaltır.
Flutter'da yerel veritabanı şeması nasıl migrate edilir?
schemaVersion getter'ını her şema değişikliğinde arttırır, migration getter'ında onCreate (temiz kurulum) ve onUpgrade (yükseltme) stratejilerini tanımlarsın. onUpgrade içindeki if (from < N) kontrolleri kümülatif olmalı, çünkü kullanıcılar sürümleri sırayla değil, birkaçını birden atlayarak günceller.
Güncelleme (Eylül 2026)
Bu makale 2025-03-25 tarihindeki drift/connectivity_plus sürümlerine göre yazıldı. O tarihten bu yana üç değişiklik gövdedeki örnekleri etkileyebilir:
- Native tam-metin arama: drift 2.35.0 ile (Eylül 2026 civarı) Dart API'sine
match,matchExp,highlight,snippet,bm25,rankgibi FTS5 tabanlı tam-metin arama fonksiyonları eklendi — yerel arama gerektiren offline-first uygulamalarda artık ayrı bir FTS kurulumuna gerek kalmadan drift üzerinden erişilebiliyor. - Transaction ve migration davranışı sıkılaştı: drift'in yeni sürümlerinde transaction başlatma davranışı
BEGIN IMMEDIATE'e geçti (eşzamanlı yazmalarda kilitlenme davranışını etkiler) ve step-by-step migration'da bir downgrade denemesi artık sessizce geçmek yerine hata fırlatıyor. Yukarıdaki migration kod örneğini güncel bir drift sürümüne taşırken bu iki davranış değişikliğini gözden geçir. - Realm/MongoDB Atlas Device Sync 30 Eylül 2025'te resmen kapandı: Bu, offline-first Flutter mimarisinde "hangi yerel-DB + hangi senkron motoru" tartışmasını drift + ayrı bir senkron katmanı (bu makaledeki outbox deseni gibi elle kurulan ya da üçüncü parti bir senkron motoru) kombinasyonuna doğru daha da kaydırdı.
Sonradan yayımlanan ilgili yazılar:
- Flutter Clean Architecture: Katmanlı Mimari Rehberi
- Flutter Riverpod ile State Management
- Flutter Test Rehberi
- Flutter Firebase Entegrasyonu: Tam Rehber
- Flutter Performans Optimizasyonu: 60fps Garanti Rehberi
- Flutter 4 Impeller: Yeni Render Engine
Sonuç
Offline-first mimari, "internet yoksa bekle" ekranını ortadan kaldırmanın tek yolu değil ama Flutter'da en olgun yoludur: drift ile tip-güvenli bir yerel şema kurar, outbox pattern ile yazmaları kuyruğa alır, bağlantı durumunu izleyip senkronu otomatikleştirir, çakışmaları veri modeline uygun stratejiyle çözersin. Bu parçaların hiçbiri tek başına karmaşık değil — zorluk, hepsini baştan (UI'yı yazdıktan sonra değil) tasarlamakta.
Firestore'un otomatik bulut-cache'i, bu makaledeki elle tasarlanmış yerel mimariden farklı bir sözleşme sunuyor, ikisi birbirinin yerine geçmiyor.
Kaynaklar
- drift — Getting started — tablo tanımı, kod üretimi ve veritabanı sınıfı kurulumu
- drift — Migrations —
MigrationStrategy,onCreate/onUpgradeAPI'si - drift — pub.dev sürüm API'si — paket sürüm geçmişi (pub.dev JSON API)
- drift — changelog — sürüm bazlı değişiklik geçmişi
- drift — Encryption — şifreli
NativeDatabasekurulumu ve mevcut veritabanını şifreleme adımları - connectivity_plus — pub.dev sürüm API'si — paket sürüm geçmişi (pub.dev JSON API)
- connectivity_plus — changelog — sürüm bazlı değişiklik geçmişi
- connectivity_plus — API dokümantasyonu —
checkConnectivity(),onConnectivityChangeddavranışı - MongoDB Atlas Device Sync — Deprecation — Device Sync kullanımdan kaldırma duyurusu

