Tüm Yazılar
KategoriFlutter
Okuma Süresi
12 dk
Yayın Tarihi
2025-03-25
Kelime Sayısı
2.497kelime

Kahveni hazırla - bu içerikli bir makale!

Flutter'da Offline-First Mimari: Drift ile Yerel Veritabanı

Özet

Flutter'da drift ile tip-güvenli yerel veritabanı kurulumu, şema migration stratejisi, outbox pattern ile yazma kuyruğu ve çakışma çözümüyle gerçek bir offline-first mimari inşa et.

  • drift, tabloları Dart sınıfı olarak tanımlayıp derleme zamanında tip-güvenli sorgu üretir; ham SQL string hatalarını runtime yerine derlemede yakalar.
  • Şema migration'ı schemaVersion + onUpgrade ile kümülatif (if from < N) yazılmalı; aksi halde birden fazla sürümü birden atlayan kullanıcılar çöküşle karşılaşır.
  • Outbox pattern, yazmayı asıl veriyle aynı transaction'da bir kuyruk tablosuna kaydeder; bağlantı geri geldiğinde bu kuyruk sırayla sunucuya boşaltılır.
  • Çakışma çözümünde last-write-wins basit ama veri kaybına açık, alan bazlı merge daha güvenli ama daha karmaşıktır; seçim veri modeline göre yapılmalı.
Flutter'da Offline-First Mimari: Drift ile Yerel Veritabanı

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?

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.0
3 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 @override
6 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// Ekleme
2await database.into(database.todoItems).insert(
3 TodoItemsCompanion.insert(title: 'Rapor yaz', updatedAt: Value(DateTime.now())),
4);
5 
6// Okuma
7final 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@override
2int get schemaVersion => 3;
3 
4@override
5MigrationStrategy 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:

  1. Yazma işlemi doğrudan sunucuya gitmez; önce yerel bir pending_operations (outbox) tablosuna kaydedilir, aynı transaction içinde asıl veriyle birlikte.
  2. 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.
  3. Asıl veri tablosundaki isSynced bayrağı, outbox kaydı başarıyla temizlendiğinde true olur.
dart
1class PendingOperations extends Table {
2 IntColumn get id => integer().autoIncrement()();
3 TextColumn get entityId => text()();
4 TextColumn get operationType => text()(); // create / update / delete
5 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 updatedAt zaman 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 olabilir
3 
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:

  1. checkConnectivity() tekil bir sonuç değil, liste döner — cihaz aynı anda hem Wi-Fi hem mobil veri arayüzüne sahip olabilir.
  2. onConnectivityChanged yalnı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ı schemaVersion kombinasyonları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 NativeDatabase kurulumunu 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, rank gibi 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:

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

Etiketler

#Flutter#drift#SQLite#offline-first#connectivity_plus#senkronizasyon#yerel veritabanı
Muhittin Çamdalı

Muhittin Çamdalı

Lead Mobile Engineer

12+ yıllık deneyime sahip Lead Mobile Engineer. Swift, SwiftUI, Kotlin ve Flutter ile iOS, Android ve cross-platform mimarilerde uzman. Performanslı ve kullanıcı dostu mobil uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.

Paylaş

İlgili İçerik