İçeriğe geç
Tüm yazılar

2026'da Android'de local-first veriyi yedeklemek: SAF export, Auto Backup ve güvenebileceğiniz bir geri yükleme akışı

Local-first Android uygulamaları kullanıcı verisini sunucu olmadan nasıl yedekler: SAF export/import, Auto Backup for App Data ve üzerine yazmadan önce doğrulayan bir geri yükleme.

MFKAPPS 4 dk okuma

“Sunucu yok” bir local-first uygulamanın bütün vaadidir. Aynı zamanda bir fabrika ayarlarına sıfırlamanın, kaybolan bir telefonun ya da bir cihaz yükseltmesinin, Granyn’deki aylarca süren bütçe geçmişini, geri yükleyecek hiçbir şey kalmadan silebilmesinin nedenidir. Bulut yoksa, yedekleme sonradan akla gelen bir ayrıntı olamaz — korumakta olduğu veri modeliyle aynı özenle inşa edilmiş bir özellik olmak zorundadır.

Burada önemli olan ve birbirinden farklı sorunları çözen iki mekanizma var. Hiçbiri diğerinin yerini tutmaz.

Auto Backup for App Data: otomatik ama opak

Eğer bunu tercih ederseniz, Android uygulamanızın dosyalarını kullanıcının Google Drive’ına otomatik olarak ve ücretsiz şekilde yedekler:

<!-- AndroidManifest.xml -->
<application
    android:allowBackup="true"
    android:fullBackupContent="@xml/backup_rules"
    ...>
<!-- res/xml/backup_rules.xml -->
<full-backup-content>
    <include domain="database" path="granyn.db" />
    <exclude domain="database" path="granyn.db-wal" />
    <exclude domain="sharedpref" path="device_keys.xml" />
</full-backup-content>

Bu gerçekten kullanışlı — hiçbir zaman bir ayarlar ekranı açmayan birini kurtaran güvenlik ağı budur. Ama karşı çıkmak yerine etrafında planlama yaptığım gerçek sınırları var:

  • Toplamda 25MB, aşarsanız sessizce kırpılır. Yıllarca işlem geçmişi artı ekran görüntüleri içeren bir Room veritabanı bu sınıra beklediğinizden daha hızlı ulaşabilir.
  • Yalnızca aynı Google hesabıyla oturum açılmış yeni bir cihazdaki kutudan çıkarma (out-of-box) kurulum akışı sırasında otomatik olarak geri yükler. Uygulamayı aynı telefona yeniden yükleyen bir kullanıcı bu yolla verilerini geri alamaz.
  • Opaktır. Neyin ne zaman yedeklendiğine dair uygulama içinde bir onay yoktur. Kullanıcıya “son yedekleme 3 gün önce” diyemiyorum, çünkü işletim sistemi bunu bana söylemiyor.

İyi bir varsayılan, kötü bir birincil strateji. Bu, yedeklemeyi hiç düşünmeyen insanlar için bir yedekleme — endişeli bir kullanıcıyı yönlendireceğim yöntem değil.

Açık export: kullanıcının gerçekten kontrol ettiği yöntem

Gerçekten güvendiğim mekanizma, kullanıcının tetiklediği ve Storage Access Framework aracılığıyla dosyayı seçtiği herhangi bir yere yazan basit bir export/import işlemidir:

@Serializable
data class GranynExport(
    val schemaVersion: Int = 2,
    val exportedAt: Long,
    val accounts: List<AccountDto>,
    val transactions: List<TransactionDto>,
)

suspend fun exportTo(context: Context, uri: Uri) {
    val export = GranynExport(
        exportedAt = clock.nowMillis(),
        accounts = accountDao.getAll().map { it.toDto() },
        transactions = transactionDao.getAll().map { it.toDto() },
    )
    context.contentResolver.openOutputStream(uri)?.use { out ->
        out.write(Json.encodeToString(export).toByteArray())
    }
}

ACTION_CREATE_DOCUMENT’tan tetiklenir, böylece hedefi kullanıcı seçer — cihaz depolaması, bir USB sürücü, zaten senkronize ettiği herhangi bir bulut klasörü. Tek seferlik seçiciden başka bir izin gerekmez ve dosya sade, incelenebilir bir JSON’dur. Bu son kısım kulağa geldiğinden daha önemlidir: yedeği bir metin editöründe açıp kendi hesap adlarını görebilen bir kullanıcı, opak bir .bak blobunun asla kazanamayacağı bir şekilde ona güvenir.

schemaVersion, gelecekteki beni kurtaran alandır. Granyn’in veri modeli lansmandan bu yana zaten bir kez değişti — yeni bir alan, sürümü artırdığım ve importer’ın, mevcut olandan tahmin etmek yerine hangi şekli beklemesi gerektiğini bildiği anlamına gelir.

Geri yükleme, herhangi bir şeyin üzerine yazma hakkını kazanmak zorunda

Import, bir yedekleme özelliğinin gerçekten test edildiği yerdir ve paranoyak olmaya değer kısımdır. Hata modu “dosya bozuk” değildir — “dosya geçerli ama kısmi bir yazma işlemi kullanıcıyı başladığından daha az veriyle bırakıyor” durumudur. Geri yüklemeyi bağlı tuttuğum kurallar:

  1. Canlı veritabanına dokunmadan önce ayrıştırın ve doğrulayın. schemaVersion’ı kontrol edin, dosyanın boş olmadığını kontrol edin, referans verilen ID’lerin içsel olarak tutarlı olduğunu kontrol edin.
  2. Kaydetmeden önce bir özet gösterin. “12 hesap, 340 işlem, 24 Temmuz’da yedeklendi” — herhangi bir şeyin üzerine yazılmadan önce, kullanıcının hatırladıklarıyla karşılaştırıp mantık kontrolü yapabileceği bir rakam.
  3. Yazma işlemini tek bir Room transaction’ına sarın. Ya import’un tamamı gerçekleşir ya da hiçbiri gerçekleşmez; import ortasında bir çökme, veritabanını yamalı bir durumda bırakamaz.
suspend fun restoreFrom(export: GranynExport) = db.withTransaction {
    require(export.schemaVersion <= CURRENT_SCHEMA_VERSION) {
        "Backup was made with a newer app version"
    }
    accountDao.deleteAll()
    transactionDao.deleteAll()
    accountDao.insertAll(export.accounts.map { it.toEntity() })
    transactionDao.insertAll(export.transactions.map { it.toEntity() })
}

db.withTransaction, gerçek güvenlik işini yapan şeydir — eğer insertAll yarı yolda bir hata fırlatırsa, Room bütün bloğu geri alır ve kullanıcının mevcut verisi dokunulmamış kalır. Bu olmadan, yarı yolda başarısız olan bir geri yükleme, hiç geri yükleme özelliği olmamasından daha kötüdür.

Bunu bir ayarlar satırı değil, çekirdek bir özellik gibi ele alın

Export/import’u Settings içine gömülü tek bir düğme olarak yayınlayıp işi bitmiş saymak cazip gelir. Local-first bir uygulama için o düğme, felaket kurtarma planının ta kendisidir — içindeki bir hatayı sessizce örtbas edecek bir sunucu tarafı yedeği yoktur. Geri yükleme yolunu, onboarding ile aynı ciddiyetle test ederim: gerçek export edilmiş dosyalar, kilitli ve yeniden başlatılmış bir telefon, sıfırdan bir kurulum. Export düğmesi bozuksa, kimse buna en çok ihtiyaç duyduğu ana kadar bunu fark etmez — ki bu da bunu keşfetmek için tam olarak yanlış zamandır.

Bunların hiçbiri karmaşık bir mühendislik değil. Sadece bir kullanıcının buna gerçekten ihtiyaç duyduğu o tek gün karşılığını veren sıkıcı türden bir iş — ki bu da, local-first bir uygulama için, var olmasının bütün sebebidir.