• 26.08.2026 21:47:31
  • Admin Admin

Flutter mobil uygulama geliştirme projelerinde yerel veritabanı şeması değiştikçe veri kaybetmeden migration yazmayı, Drift sorgularını indekslemeyi, SQLCipher anahtarlarını yönetmeyi ve gerçek cihazda doğrulamayı ele alır.

Flutter Mobil Uygulama Geliştirmede Drift ile Şema Migrasyonu

Flutter mobil uygulama geliştirme için migration sözleşmesini kurmak

Bir flutter mobil uygulama geliştirme uygulamasında migration, sadece eski kolonu yenisine çevirmek değildir. Uygulama mağazada kademeli dağıtıldığı için aynı API'ye bağlanan v1, v2 ve v3 istemcileri günlerce birlikte çalışabilir. Bu nedenle her şema değişikliğinde şu üç değeri PR açıklamasına yazın: eski sürümden yeni sürüme geçiş SQL'i, geri dönüş davranışı ve veri dönüştürme süresi. Drift'te schemaVersion artırılmadan eklenen tablo veya kolon, temiz kurulumda çalışıp mevcut cihazda 'no such column' hatası üretir.

Örneğin silinen hesapları fiziksel olarak kaldırmak yerine deletedAt ile işaretlemek, senkronizasyon kuyruğunun silme olayını sunucuya ulaştırmasına izin verir. Aşağıdaki şemada lastSyncedAt nullable seçilmiştir; nullable olmayan bir kolonu mevcut tabloya varsayılan değer vermeden eklemek SQLite migration'ında doğrudan hata üretir. Bu ayrıntı, flutter eğitimi veya flutter kursu sırasında genellikle temiz veritabanı üzerinden yapılan demoların gizlediği üretim hatalarındandır.

import 'package:drift/drift.dart';

class Accounts extends Table {
  TextColumn get id => text()();
  TextColumn get tenantId => text()();
  TextColumn get email => text().unique()();
  DateTimeColumn get deletedAt => dateTime().nullable()();
  DateTimeColumn get lastSyncedAt => dateTime().nullable()();

  @override
  Set<Column> get primaryKey => {id};
}

@DriftDatabase(tables: [Accounts])
class AppDatabase extends _$AppDatabase {
  AppDatabase(super.executor);

  @override
  int get schemaVersion => 2;

  @override
  MigrationStrategy get migration => MigrationStrategy(
    onCreate: (m) async => m.createAll(),
    onUpgrade: (m, from, to) async {
      if (from < 2) {
        await m.addColumn(accounts, accounts.lastSyncedAt);
      }
    },
    beforeOpen: (details) async {
      await customStatement('PRAGMA foreign_keys = ON');
    },
  );
}

PRAGMA foreign_keys = ON her SQLite bağlantısında ayrı ayrı etkinleştirilmelidir; bir kez migration içinde çalıştırmak yeterli değildir. Drift'in açılış kancasına koymak, geliştirme ortamı ile üretimdeki bağlantı davranışını eşitler. Foreign key kapalıyken ebeveyn kaydı silinip çocuk satırlar kalabilir; sorun aylar sonra bir join sonucunda görünür ve migration hatası sanılır.

Drift migration'ını veri kopyalamadan önce test etmek

Migration testi için yalnızca flutter test ile boş veritabanı açmak yeterli değildir. CI içinde eski şemayla oluşturulmuş bir fixture veritabanını uygulama sandbox'ına kopyalayın, güncel AppDatabase ile açın ve hem satır içeriğini hem de PRAGMA user_version değerini doğrulayın. Drift şema çıktısını sürümleyerek gözden geçirilebilir hale getirmek için şu komutu her şema değişikliğinde çalıştırın:

dart run drift_dev schema dump lib/data/app_database.dart drift_schemas/
git add drift_schemas/
flutter test test/database/migration_test.dart

Fixture testinde en az bir gerçek kullanıcı verisi, nullable olmayan alan, UTF-8 karakter ve silinmiş kayıt bulunmalıdır. Aşağıdaki test, migration sonrası eski satırın korunmasını ve yeni kolonun beklenen null durumunu ölçer. Test veritabanını test içinde güncel şemayla oluşturursanız migration yolu hiç çalışmaz; fixture'ın gerçekten v1 dosyası olması kritik noktadır.

test('v1 fixture migrates without losing account data', () async {
  final file = await copyFixtureToTemporaryDirectory('accounts_v1.sqlite');
  final db = AppDatabase(NativeDatabase(file));

  final rows = await db.select(db.accounts).get();

  expect(rows, hasLength(1));
  expect(rows.single.email, 'ayse@example.com');
  expect(rows.single.lastSyncedAt, isNull);
  await db.close();
});

Kolon adı değiştirirken ALTER TABLE ... RENAME COLUMN kullanmak her hedef platformdaki SQLite derlemesinin davranışına güvenmek anlamına gelebilir. Daha denetlenebilir yaklaşım yeni tablo oluşturmak, seçili kolonları açıkça kopyalamak, sayımları karşılaştırmak ve sonra eski tabloyu silmektir. Kopyalama işleminden önce SELECT count(*), işlemden sonra yeni tablodan aynı sayımı alıp loglayın; özellikle filtreli kopyalarda satır kaybını bu sayede yakalarsınız.

Cross platform mobil uygulama geliştirme ve şifreli yerel veri

cross platform mobil uygulama geliştirme bağlamında 'veritabanını şifrele' kararı platformlara aynı kodu yazmak değildir. Android ve iOS'ta SQLCipher ile dosya şifreleme uygulanabilirken web hedefinde aynı SQLite dosya modeli yoktur; IndexedDB ve WebCrypto için ayrı bir tehdit modeli gerekir. SQLCipher anahtarını veritabanı dosyasının yanına, SharedPreferences içine veya kaynak koda koymayın. Anahtarı flutter_secure_storage ile Android Keystore ve iOS Keychain korumalı depoya yazın:

import 'dart:convert';
import 'dart:math';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

const storage = FlutterSecureStorage();

Future<String> loadOrCreateDatabaseKey() async {
  const keyName = 'sqlcipher_key_v1';
  final existing = await storage.read(key: keyName);
  if (existing != null) return existing;

  final bytes = List<int>.generate(32, (_) => Random.secure().nextInt(256));
  final key = base64UrlEncode(bytes);
  await storage.write(key: keyName, value: key);
  return key;
}

SQLCipher kullanan bağlantıda anahtar, herhangi bir tablo sorgusundan önce uygulanmalıdır; aksi halde ilk şema sorgusu 'file is not a database' hatası verir. Paketinizin SQLCipher destekli executor'ı veya sürücüsü anahtarı açılışta vermiyorsa, sıradan SQLite executor'ını sonradan PRAGMA key ile düzeltmeye çalışmayın. Ayrıca SQLCipher'ın PRAGMA cipher_integrity_check komutunu test cihazında çalıştırın; dosyanın açılması tek başına bütün sayfaların doğrulanmış olduğu anlamına gelmez.

Sık atlanan edge case yedekten geri yüklemedir: Android'de uygulama veritabanı geri gelirken Keystore ile sarılmış anahtar geri gelmeyebilir. Açılışta anahtar var ama şifreli dosya açılamıyorsa dosyayı sessizce silmek yerine kullanıcı oturumunu ve senkronizasyon durumunu dikkate alan kontrollü bir kurtarma akışı tasarlayın. Sunucudan yeniden indirilemeyen taslak veriler için bu davranış veri kaybı demektir.

Flutter state management ile veritabanı akışlarını bağlamak

flutter state management katmanında DAO'nun watch() akışını doğrudan ekrana yaymak yerine, sorgu sınırını repository içinde tutun. Aşağıdaki Riverpod sağlayıcısı, aktif hesap listesini tek bir sorgudan yayınlar. Drift aynı tabloyu etkileyen yazılarda sorguyu yeniden çalıştırır; bu nedenle widget ağacında elle 'refresh' bayrağı taşımak yerine veritabanı değişikliği olayını kaynak kabul edebilirsiniz.

final activeAccountsProvider = StreamProvider.autoDispose((ref) {
  final db = ref.watch(databaseProvider);
  final query = db.select(db.accounts)
    ..where((a) => a.deletedAt.isNull())
    ..orderBy([(a) => OrderingTerm.asc(a.email)]);
  return query.watch();
});

Bir senkronizasyon turunda 500 satırı tek tek yazıp her yazıdan sonra UI durumunu güncellerseniz, akış aboneleri gereksiz sorgular çalıştırabilir. Yazıları transaction içinde gruplayın ve Flutter DevTools Performance görünümünde senkronizasyon öncesi-sonrası frame zamanlarını karşılaştırın. Aynı zamanda DevTools CPU Profiler'da sqlite3_step, JSON dönüştürme ve widget build örneklerinin sayısına bakın; yalnızca ortalama süre değil, 16.67 ms üstündeki frame sayısı karar vermek için anlamlıdır.

await db.transaction(() async {
  await db.batch((batch) {
    batch.insertAllOnConflictUpdate(db.accounts, incomingAccounts);
  });
});

Harici bir native bağlantının aynı SQLite dosyasına yazması ayrı bir problemdir: Drift'in Dart tarafındaki stream invalidation mekanizması bu yazıyı her zaman göremez. Native kodla yazmak zorundaysanız yazma sonrasında repository'nin yenileme protokolünü açıkça tasarlayın veya tüm yazıları aynı Drift executor'ından geçirin. dart programlama eğitimi içeriklerinde stream'in kendiliğinden her dış değişikliği izleyeceği varsayımı, özellikle platform kanalı kullanan ekiplerde bayat ekranlara yol açar.

İndeks kararını EXPLAIN QUERY PLAN ile ölçmek

İndeks eklemek varsayımla yapılmamalıdır. Senkronizasyon sorgunuz tenant, silinme durumu ve güncellenme zamanına göre filtreliyorsa önce gerçek üretim benzeri satır sayısıyla EXPLAIN QUERY PLAN çalıştırın. Çıktıda SCAN accounts görüyorsanız SQLite tüm tabloyu dolaşıyordur; milyon satır gerekmese bile düşük uç cihazda disk erişimi ve satır nesnesi oluşturma maliyeti görünür hale gelir.

EXPLAIN QUERY PLAN
SELECT id, email, last_synced_at
FROM accounts
WHERE tenant_id = ?
  AND deleted_at IS NULL
  AND last_synced_at < ?
ORDER BY last_synced_at
LIMIT 100;

Bu sorgu için migration'a aşağıdaki bileşik indeksi ekleyip aynı cihazda önce-sonra ölçümü alın. Eşitlik filtreleri başta, aralık ve sıralama alanı sonda olduğunda B-tree daha dar bir aralıkta gezinir. deleted_at IS NULL gibi düşük seçicilikli bir alanı tek başına indekslemek ise çoğu zaman ek yazma maliyetine değmez.

await customStatement('''
  CREATE INDEX IF NOT EXISTS accounts_sync_lookup
  ON accounts(tenant_id, deleted_at, last_synced_at)
''');

Ölçüm kaydına sorgu süresi yanında PRAGMA page_count ve PRAGMA page_size sonuçlarını da ekleyin; indeks dosyayı büyütür ve her insert veya update'te güncellenir. Hedef, en fazla indeks sayısı değil, DevTools zaman çizelgesinde senkronizasyon sırasında görülen pahalı sorguyu planlı bir index seek'e çevirmektir. Bu yaklaşım flutter kursu örneklerindeki küçük veri kümelerinden üretim verisine geçerken yanlış indeks kararlarını azaltır.

İlgili Eğitim

Flutter Eğitimi

Sık Sorulan Sorular

flutter eğitimi sırasında Drift migration testi nasıl yazılır?

Güncel şemayla oluşturulmuş bellek içi veritabanını test etmeyin. Eski schemaVersion ile hazırlanmış bir SQLite fixture dosyasını geçici dizine kopyalayın, güncel AppDatabase ile açın, satır içeriklerini ve yeni kolon varsayımlarını assert edin. CI'da `dart run drift_dev schema dump` çıktısını da sürümleyin.

flutter state management ile Drift watch kullanırken neden transaction gerekir?

Bir senkronizasyon paketindeki insert ve update işlemlerini `db.transaction(() async { ... })` içinde gruplayın. Aksi halde her ara yazma abonelerin sorgusunu yeniden tetikleyebilir. Flutter DevTools Performance görünümünde transaction öncesi ve sonrası 16.67 ms üstü frame sayısını, CPU Profiler'da SQLite çağrılarını karşılaştırın.

cross platform mobil uygulama geliştirme için SQLCipher anahtarı nerede tutulmalı?

Anahtarı `flutter_secure_storage` ile Android Keystore ve iOS Keychain'de tutun; SharedPreferences, uygulama sabitleri ve veritabanı klasörü uygun değildir. SQLCipher anahtarı ilk sorgudan önce executor'a verilmelidir. Android yedek geri yüklemesinde veritabanı ile Keystore anahtarının eşleşmeyebileceğini ayrıca test edin.

flutter mobil uygulama geliştirme projesinde SQLite indeksi ne zaman eklenmeli?

Önce üretime benzer veriyle `EXPLAIN QUERY PLAN` çalıştırın. `SCAN tablo_adi` ile başlayan plan ve ölçülebilir sorgu gecikmesi varsa, WHERE koşullarındaki eşitlik alanlarını ve ardından aralık veya ORDER BY alanını içeren bileşik indeks deneyin. Değişiklikten sonra aynı sorgu süresini, page_count değerini ve yazma süresini birlikte ölçün.

AI / LLM Discovery

Bu makale Opendart Akademi Flutter eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.

Opendart Akademi llms.txt