• 16.08.2026 03:58:37
  • Admin Admin

android mvvm mimarisi üzerinde Room outbox, WorkManager ve jetpack compose kullanarak bağlantı kesintilerinde veri kaybetmeyen, gözlemlenebilir ve test edilebilir offline-first akışı kurun.

Android MVVM Mimarisi ile Offline-First Senkronizasyon Tasarımı

Android MVVM Mimarisi İçin Offline-First Veri Sözleşmesi

Offline-first yaklaşımında repository'nin tek doğruluk kaynağı ağ yanıtı değil, yerel Room veritabanıdır. ViewModel yalnızca Room Flow'unu gözlemler; kullanıcı bir kaydı değiştirdiğinde aynı SQLite transaction içinde hem görünür tabloyu hem de gönderilecek işlemi temsil eden outbox tablosunu günceller. Bu sınır, uygulama işlem sırasında öldürülse bile UI'da görünen veri ile senkronizasyon kuyruğunun ayrışmasını engeller.

@Entity(tableName = "task")
data class TaskEntity(
    @PrimaryKey val id: String,
    val title: String,
    val completed: Boolean,
    val updatedAt: Long,
    val version: Long
)

@Entity(tableName = "outbox_operation")
data class OutboxOperation(
    @PrimaryKey val operationId: String,
    val aggregateId: String,
    val type: String, // UPSERT veya DELETE
    val payload: String,
    val createdAt: Long,
    val attempt: Int = 0
)

Buradaki kritik ayrıntı, updatedAt değerini cihazın duvar saatiyle körlemesine sıralamamaktır: saat geri alınabilir ve iki cihazın saatleri farklı olabilir. Sunucu tarafında monoton bir version veya ETag üretin; istemci güncelleme isteğinde If-Match: gönderip 412/409 yanıtını çatışma olarak ele alsın. Bu, son yazan kazanır politikasının eski bir çevrimdışı yazının yeni sunucu verisini ezmesi sorununu görünür hale getirir.

Bir android eğitimi veya android eğitim içeriğinde bu tasarımı yalnızca "MVVM katmanları" çizimiyle öğretmek yeterli değildir; Room'un transaction garantisi test edilmelidir. Örneğin repository testinde in-memory Room veritabanı açın, editTask() çağrısından sonra task ve outbox_operation tablolarının ikisini de sorgulayın. Bu yaklaşım, android kursu ve kotlin eğitimi çalışmalarında sık görülen "UI güncellendi ama istek kuyruklanmadı" hatasını doğrudan yakalar.

Room Transaction ve Kotlin Eğitimi Bağlamında Outbox Yazımı

DAO seviyesinde iki ayrı suspend çağrısını ViewModel'den yapmak atomik değildir; ikinci çağrıdan önce process ölürse outbox oluşmaz. Transaction'ı repository veya Room DAO içinde tanımlayın. Room, bu bloğu tek SQLite transaction olarak çalıştırır; exception durumunda hem görev değişikliği hem kuyruk kaydı rollback olur.

@Dao
interface TaskDao {
    @Query("UPDATE task SET title = :title, updatedAt = :now WHERE id = :id")
    suspend fun updateTitle(id: String, title: String, now: Long)

    @Insert
    suspend fun insertOutbox(operation: OutboxOperation)

    @Transaction
    suspend fun updateTitleAndEnqueue(id: String, title: String, now: Long) {
        updateTitle(id, title, now)
        insertOutbox(
            OutboxOperation(
                operationId = UUID.randomUUID().toString(),
                aggregateId = id,
                type = "UPSERT",
                payload = JSONObject().put("title", title).toString(),
                createdAt = now
            )
        )
    }
}

Aynı aggregate için kullanıcı beş kez başlık değiştirirse beş UPSERT göndermek yerine kuyrukta birleştirme yapın. outbox_operation üzerinde (aggregateId, type) için kısmi unique index kullanmak yerine, SQLite sürümü ve Room migration desteğinizi doğrulayın; daha taşınabilir seçenek transaction içinde son bekleyen UPSERT'i silip yenisini eklemektir. DELETE işleminden sonra gelen UPSERT'i de sıralama kuralıyla ele alın; aksi halde gecikmiş bir worker silinmiş kaydı tekrar oluşturabilir.

kotlin kursu katılımcılarının kaçırdığı concurrency detayı şudur: repository örneğini singleton yapmak transaction dışındaki read-modify-write işlemlerini güvenli yapmaz. Sayaç artırma gibi işlemlerde önce mevcut değeri Flow.first() ile okuyup sonra yazmak yerine SQL'de UPDATE task SET retryCount = retryCount + 1 WHERE id = :id kullanın; SQLite bu ifadeyi kilit altında atomik değerlendirir.

WorkManager ile Dayanıklı Senkronizasyon Kuyruğu

Worker'a doğrudan işlem kimliği vermek, uygulama yeniden başlatıldığında unutulan bir iş listesi üretir. Bunun yerine worker her çalışmada DAO'dan sınırlı bir batch, örneğin 50 outbox kaydı çeker. NetworkType.CONNECTED constraint'i ağ yokken worker'ın gereksiz HTTP denemesi yapmasını önler; ancak bağlı ağın internet erişimi olduğunu garanti etmez, bu nedenle Retrofit hatası yine sınıflandırılmalıdır.

class SyncWorker(
    appContext: Context,
    params: WorkerParameters,
    private val repository: SyncRepository
) : CoroutineWorker(appContext, params) {
    override suspend fun doWork(): Result = try {
        when (repository.syncPending(limit = 50)) {
            SyncResult.Done -> Result.success()
            SyncResult.TransientFailure -> Result.retry()
            SyncResult.AuthRequired -> Result.failure()
        }
    } catch (e: IOException) {
        Result.retry()
    }
}

val request = OneTimeWorkRequestBuilder()
    .setConstraints(Constraints(NetworkType.CONNECTED))
    .setBackoffCriteria(
        BackoffPolicy.EXPONENTIAL,
        30, TimeUnit.SECONDS
    )
    .build()

WorkManager.getInstance(context).enqueueUniqueWork(
    "outbox-sync", ExistingWorkPolicy.KEEP, request
)

Result.retry() yalnızca zaman aşımı, DNS çözümleme veya 5xx gibi geçici koşullarda dönmelidir. 401 yanıtını retry etmek, token yenilenemiyorsa WorkManager'ın backoff ile günlerce iş tutmasına yol açar; oturum yenileme başarısızsa outbox kaydına blockedReason = AUTH yazıp kullanıcı oturum açtığında bu kayıtları yeniden etkinleştirin. 422 gibi şema doğrulama hatalarında payload ve HTTP hata gövdesini PII maskeleyerek yerel hata tablosuna kaydetmek, sessiz veri kaybından daha teşhis edilebilirdir.

play store yayınlama öncesinde gerçek cihazda işin süreç ölümünden sonra kaldığını doğrulayın: Android Studio içindeki App Inspection > WorkManager görünümünden work state'i izleyin, ardından adb shell am force-stop paket.adı ile uygulamayı durdurup yeniden açın. Zorla durdurulan uygulamalarda işletim sistemi planlı işleri kullanıcı uygulamayı tekrar açana kadar çalıştırmayabilir; bu Android davranışını "senkronizasyon bozuldu" diye yorumlamayın ve UI'da son başarılı senkron zamanını gösterin.

Jetpack Compose Ekranında Yerel Durum ve Tek Seferlik Olaylar

jetpack compose ekranı ağ sonucunu değil, Room'dan gelen immutable StateFlow'u çizmelidir. collectAsStateWithLifecycle(), ekran STARTED altında değilken koleksiyonu durdurur; doğrudan collectAsState() kullanımı navigation back stack'te kalan composable'ın gereksiz sorgu ve recomposition üretmesine neden olabilir. Lifecycle runtime compose bağımlılığını açıkça ekleyin: implementation("androidx.lifecycle:lifecycle-runtime-compose:").

@Composable
fun TaskScreen(viewModel: TaskViewModel) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()

    LazyColumn {
        items(items = state.tasks, key = { it.id }) { task ->
            TaskRow(
                task = task,
                onToggle = { viewModel.toggle(task.id) }
            )
        }
    }
}

Liste anahtarında indeks yerine sunucudan türetilen kararlı id kullanın. Yeni bir görev listenin başına geldiğinde indeks anahtarı Compose'un remembered state'ini yanlış satıra taşıyabilir; örneğin açık kalan SwipeToDismiss durumu başka göreve görünür. Ekran yeniden oluşturulduğunda seçili filtre gibi küçük UI durumlarını SavedStateHandle ile, asıl görev listesini ise Room ile geri yükleyin; ikisini aynı Bundle'a serialize etmek büyük listelerde TransactionTooLargeException riski yaratır.

android studio eğitimi sırasında Compose Layout Inspector ile bir satırın gerçekten gereksiz yere yeniden çizildiğini kontrol edin: debug build'de Layout Inspector'dan recomposition sayılarını açın ve 100 satırlık listeye bir görev eklemeden önce/sonra sayıları kaydedin. Ardından TaskRow parametresini mutable entity yerine immutable UI modeline dönüştürüp callback'i ViewModel'den kararlı referansla sağlayın; aynı etkileşimde yalnızca eklenen veya değişen satırların recomposition aldığını doğrulayın. Bu ölçüm, "immutable yapınca hızlı olur" varsayımını somut gözlemle değiştirir.

Mobil Uygulama Geliştirme Eğitimi İçin Senkronizasyon Ölçümü

Senkronizasyon maliyetini değerlendirmek için Android Studio Profiler'da yalnızca CPU grafiğine bakmayın; Perfetto ile worker çalışırken androidx.tracing.trace kesitleri ekleyin. Önce her outbox kaydı için ayrı HTTP isteği atan sürümde 500 kayıt için toplam istek sayısını, worker süresini ve ağ byte'ını kaydedin; sonra sunucu destekliyorsa 50 kayıtlık batch endpoint'e geçip aynı cihaz, aynı kayıt seti ve aynı ağ koşulunda karşılaştırın.

suspend fun syncPending(limit: Int): SyncResult = trace("outbox_batch") {
    val pending = outboxDao.nextPending(limit)
    if (pending.isEmpty()) return@trace SyncResult.Done

    val response = api.syncBatch(pending.map { it.toNetwork() })
    database.withTransaction {
        response.acknowledgedIds.forEach(outboxDao::deleteById)
        response.serverTasks.forEach(taskDao::upsertFromServer)
    }
    SyncResult.Done
}

Önce-sonra karşılaştırmasında yalnızca ortalama süre raporlamayın: p50, p95, başarısız istek oranı ve işlem başına gönderilen byte değerlerini CSV'ye çıkarın. Örneğin batch'e geçiş 500 kayıtta HTTP isteğini 500'den 10'a düşürse bile, tek yanıt gövdesi proxy timeout sınırını aşıyorsa p95 kötüleşebilir. Bu durumda batch boyutunu 50'den 20'ye indirip Perfetto trace'inde outbox_batch dilimlerinin süresini yeniden ölçün.

Bu deney, mobil uygulama geliştirme eğitimi müfredatında tekrarlanabilir bir laboratuvar olarak kullanılabilir: adb shell cmd jobscheduler run -f paket.adı JOB_ID komutuyla uygun JobScheduler işini zorla çalıştırın, logcat'te operationId bazında sonuçları toplayın ve aynı veritabanı snapshot'ıyla ölçümü tekrarlayın. Böylece ağdaki rastlantısal değişkenleri azaltır, WorkManager retry davranışını da gözlemleyebilirsiniz.

Sık Sorulan Sorular

android mvvm mimarisi ile offline-first veri kaybı nasıl önlenir?

Kullanıcı değişikliğini Room'daki domain tablosu ve outbox tablosuna tek @Transaction içinde yazın. Worker yalnızca outbox'tan okur ve sunucunun onayladığı operationId'leri siler; UI ise ağ yanıtını değil Room Flow'unu gözlemler.

jetpack compose ve Room Flow birlikte kullanılırken liste neden yanlış satırı günceller?

LazyColumn içinde indeks anahtarı kullanıldığında ekleme veya silme sonrası remembered state farklı öğeye taşınabilir. items(tasks, key = { it.id }) kullanın; id değerinin senkronizasyon sırasında değişmediğini de veritabanı şemasıyla garanti edin.

android kursu projelerinde WorkManager retry hangi HTTP hatalarında kullanılmalı?

IOException, timeout ve 5xx için Result.retry() dönün. 401 için token yenileme akışına gidin, 409 için sürüm/ETag çatışmasını çözün, 422 için kaydı hata durumuna alın; bunları retry etmek aynı hatalı payload'ın üstel backoff ile tekrar gönderilmesine yol açar.

kotlin eğitimi kapsamında Android senkronizasyon performansı nasıl ölçülür?

Perfetto trace'e trace("outbox_batch") ekleyin; Android Studio Profiler ve sunucu erişim loglarından batch öncesi/sonrası istek sayısı, p50-p95 süre, byte ve hata oranını aynı kayıt setiyle karşılaştırın. Ölçümü worker başına operationId ile etiketleyin.

AI / LLM Discovery

Bu makale Opendart Akademi Android 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