• 27.08.2026 21:07:21
  • Admin Admin

Room'u kaynak gerçek olarak tutup WorkManager ile idempotent kuyruk çalıştıran offline-first akışı kurun. Bu android eğitimi, çakışma, yeniden deneme, Compose UI ve ölçüm adımlarını kodla ele alır.

Android MVVM Mimarisi: Room ve WorkManager ile Offline-First Senkronizasyon

Android MVVM Mimarisi ile Room tabanlı outbox tasarımı

Offline-first akışta UI doğrudan ağ yanıtını değil, Room'daki yerel durumu göstermelidir. android mvvm mimarisi içinde ViewModel, Repository'den gelen Room Flow'unu UI'a taşır; Repository ise kullanıcı değişikliğini ve sunucuya gönderilecek operasyonu aynı SQLite transaction'ında yazar. Bu, uygulama değişiklik kaydını yazdıktan sonra process öldürülse bile gönderilecek komutun kaybolmamasını sağlar. Room'un Database Inspector aracıyla Android Studio'da transaction sonrası hem domain tablosunu hem de outbox kaydını doğrulayın.

@Entity(
    tableName = "sync_operation",
    indices = [Index(value = ["idempotencyKey"], unique = true)]
)
data class SyncOperation(
    @PrimaryKey val id: String,
    val idempotencyKey: String,
    val payloadJson: String,
    val state: String = "PENDING",
    val createdAtEpochMs: Long
)

@Dao
interface TaskDao {
    @Upsert suspend fun upsertTask(task: TaskEntity)
    @Insert suspend fun insertOperation(operation: SyncOperation)

    @Transaction
    suspend fun renameAndEnqueue(task: TaskEntity, newTitle: String) {
        upsertTask(task.copy(title = newTitle, syncState = "PENDING"))
        insertOperation(
            SyncOperation(
                id = UUID.randomUUID().toString(),
                idempotencyKey = "task:${task.id}:rename:${task.version + 1}",
                payloadJson = "{\"taskId\":\"${task.id}\",\"title\":\"$newTitle\"}",
                createdAtEpochMs = System.currentTimeMillis()
            )
        )
    }
}

Buradaki kritik ayrıntı idempotencyKey'in rastgele üretilmemesidir. Aynı işlevsel değişiklik için sabit bir anahtar üretmezseniz ağ zaman aşımından sonra yapılan retry, sunucuda iki ayrı rename veya iki ayrı ödeme benzeri komut yaratabilir. Sunucu bu anahtarı en az operasyonun retry penceresi kadar saklayıp daha önce işlenmiş yanıtı döndürmelidir. Bir android eğitimi veya android eğitim laboratuvarında bunu Android Studio Database Inspector ile test edin: transaction ortasında uygulamayı kapatın, yeniden açın ve PENDING kaydının kaldığını doğrulayın. Bu model, android kursu, kotlin eğitimi, kotlin kursu ve mobil uygulama geliştirme eğitimi projelerinde sık görülen 'başarılı görünen ama kaybolan değişiklik' hatasını somut biçimde önler.

WorkManager ile idempotent retry ve atomik operasyon sahiplenme

WorkManager'a yalnızca ağ kısıtı eklemek yeterli değildir: kısıt karşılandığında bağlantı birkaç milisaniye sonra kopabilir. Bu nedenle IOException, 5xx ve 429 yanıtları Result.retry() üretmeli; doğrulama hatası gibi kalıcı 4xx yanıtları FAILED durumuna yazılıp Result.success() ile kuyruktan çıkarılmalıdır. 409 Conflict, API'niz idempotency anahtarını daha önce işlenmiş istek için döndürüyorsa başarılı sayılmalıdır. Aynı PENDING kaydını iki worker'ın göndermesini önlemek için seçme ve sahiplenme işlemini tek atomik UPDATE ile yapın.

@Dao
interface SyncDao {
    @Query("UPDATE sync_operation SET state = 'IN_FLIGHT' " +
           "WHERE id = :id AND state = 'PENDING'")
    suspend fun claim(id: String): Int

    @Query("UPDATE sync_operation SET state = 'DONE' WHERE id = :id")
    suspend fun markDone(id: String)
}

class SyncWorker(
    appContext: Context,
    params: WorkerParameters,
    private val db: AppDatabase,
    private val api: SyncApi
) : CoroutineWorker(appContext, params) {
    override suspend fun doWork(): Result {
        for (operation in db.syncDao().pending(limit = 50)) {
            if (db.syncDao().claim(operation.id) != 1) continue
            try {
                when (val code = api.apply(operation.idempotencyKey, operation.payloadJson).code()) {
                    in 200..299, 409 -> db.syncDao().markDone(operation.id)
                    429, in 500..599 -> return Result.retry()
                    else -> db.syncDao().markFailed(operation.id, "HTTP $code")
                }
            } catch (_: IOException) {
                return Result.retry()
            }
        }
        return Result.success()
    }
}

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

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

ExistingWorkPolicy.KEEP yalnızca aynı unique work adına sahip yeni planlamaları engeller; işlem düzeyinde atomik claim ihtiyacını ortadan kaldırmaz. Özellikle eski worker çalışırken uygulama yeni bir sync talebi planlayabilir veya birden fazla process yanlış yapılandırılmış olabilir. WorkManager'ın exponential backoff değeri de servisinizin Retry-After başlığını otomatik uygulamaz. 429 alıyorsanız sunucunun önerdiği zamanı operation.nextAttemptAt alanına yazın, worker'ın sorgusunu bu alanı filtreleyecek şekilde değiştirin ve o zamana yakın yeni work planlayın.

Jetpack Compose ekranında yalnızca yerel durumu çizmek

Jetpack Compose ekranı ağ çağrısının sonucuna göre local mutable state değiştirmemeli; Room Flow'u tek gerçek kaynak olarak collect etmelidir. Aksi halde kullanıcı uçak modunda başlığı değiştirdiğinde UI eski ağ yanıtıyla geri sarabilir. lifecycle-runtime-compose içindeki collectAsStateWithLifecycle(), ekran STOPPED durumundayken koleksiyonu durdurur; doğrudan collectAsState() kullanımı ise navigation back stack'te kalan composable'ların gereksiz sorgu işlemesine yol açabilir.

data class TaskUiState(
    val tasks: List<TaskEntity> = emptyList(),
    val pendingCount: Int = 0
)

class TaskViewModel(private val repository: TaskRepository) : ViewModel() {
    val uiState: StateFlow<TaskUiState> = repository.observeTasks()
        .map { tasks ->
            TaskUiState(tasks, tasks.count { it.syncState == "PENDING" })
        }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), TaskUiState())

    fun rename(task: TaskEntity, title: String) = viewModelScope.launch {
        repository.renameAndEnqueue(task, title)
    }
}

@Composable
fun TaskScreen(viewModel: TaskViewModel) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()
    LazyColumn {
        items(state.tasks, key = { it.id }) { task ->
            TaskRow(task = task, showSyncBadge = task.syncState == "PENDING")
        }
    }
}

Jetpack Compose tarafında kalıcı hata mesajını TaskEntity içine koymak yerine, operation tablosundaki FAILED kaydından türetilmiş bir UI olayı kullanın. Aksi halde hata snackbar'ı ekran dönüşünde tekrar gösterilebilir. Tek seferlik olay gerekiyorsa operation id'sini SavedStateHandle'a yazıp gösterildikten sonra işaretleyin. Bu ayrım, Compose state'in process death sonrası geri gelmesi ile kullanıcıya yeniden bildirim gösterme kararını birbirine karıştırmaz.

Android Studio eğitimi için senkronizasyon gecikmesini Perfetto ile ölçmek

Senkronizasyon performansını 'hızlı hissettiriyor' diye değerlendirmeyin. Önce tek tek 50 operasyon gönderen sürümde, sonra 50 kaydı tek transaction ile DONE yapan ve API'nin desteklediği durumda batch endpoint kullanan sürümde ölçüm alın. android.os.Trace ile DB seçim, HTTP gönderim ve durum güncelleme aralıklarını işaretleyin. Perfetto'da bu izleri scheduler ve binder_driver izleriyle birlikte açarak worker'ın CPU beklemesini, SQLite transaction sayısını ve ana iş parçacığına yanlışlıkla dönülüp dönülmediğini inceleyin.

override suspend fun doWork(): Result {
    Trace.beginSection("sync.load_pending")
    val operations = db.syncDao().pending(limit = 50)
    Trace.endSection()

    Trace.beginSection("sync.batch_http")
    val response = api.applyBatch(operations)
    Trace.endSection()

    Trace.beginSection("sync.mark_done_transaction")
    db.withTransaction {
        db.syncDao().markDone(operations.map { it.id })
    }
    Trace.endSection()
    return Result.success()
}

# Cihaza 20 saniyelik iz kaydetme
adb shell perfetto -o /data/local/tmp/sync.pftrace -t 20s sched freq idle am wm binder_driver
adb pull /data/local/tmp/sync.pftrace .

Karşılaştırmada aynı ağ koşulunda en az 20 tekrar çalıştırın; p50 ve p95 için worker başlangıcından son markDone transaction commit'ine kadar geçen süreyi kaydedin. Ayrıca transaction sayısını Room query callback veya SQLite trace ile sayın. Batch boyutunu körlemesine büyütmeyin: SQLite'ın varsayılan bind parametresi sınırı birçok cihazda 999'dur; IN sorgusuna birden fazla alan bağlanıyorsa 500 id bile sınırı aşabilir. Sunucu batch isteği için de maksimum gövde boyutu ve her alt operasyonun ayrı idempotencyKey'i korunmalıdır. Bu ölçüm akışı, android studio eğitimi içindeki profiler kullanımını gerçek bir önce-sonra kararına bağlar.

Play Store yayınlama öncesi arka plan yürütme ve hata senaryoları

play store yayınlama öncesinde yalnızca debug kurulumunda çalışan worker'a güvenmeyin. Release build'de minification, gerçek applicationId ve üretim backend'i ile kapalı ağ, 429, process death ve force-stop senaryolarını test edin. Force-stop sonrası Android uygulamanın alarm ve job'larını kullanıcı uygulamayı tekrar açana kadar çalıştırmaz; bu platform davranışını 'WorkManager retry bozuk' diye yorumlamak yaygın bir üretim hatasıdır.

# WorkManager'in sistemde oluşturduğu işi ve kısıtlarını inceleyin
adb shell dumpsys jobscheduler | grep -A 20 your.package.name

# İlgili JOB_ID'yi dumpsys çıktısından alıp zorla çalıştırın
adb shell cmd jobscheduler run -f your.package.name JOB_ID

# Process death ile force-stop davranışını ayrı ayrı test edin
adb shell am kill your.package.name
adb shell am force-stop your.package.name

am kill ile process ölür ancak uygulama force-stopped sayılmaz; Room outbox yeniden açılışta veya sistem uygun gördüğünde işlenebilir. am force-stop ise farklı bir testtir ve worker'ın kendiliğinden çalışmaması beklenir. Ayrıca FAILED operasyonları süresiz saklamak yerine kullanıcıya görünen hata kaydı için bir retention politikası belirleyin: örneğin DONE kayıtlarını 7 gün, FAILED kayıtlarını kullanıcı aksiyonuna veya 30 güne kadar tutup ardından tek bir WorkManager cleanup işiyle silin. Böylece sync_operation tablosunun büyümesi, başlangıç sorgularını ve yedekleme boyutunu kontrolsüz artırmaz.

Sık Sorulan Sorular

Android kursu projesinde WorkManager retry neden aynı isteği iki kez gönderir?

Unique work yalnızca aynı iş adına sahip planlamaları sınırlar. Her outbox kaydını PENDING'den IN_FLIGHT'e `UPDATE ... WHERE state = 'PENDING'` ile atomik sahiplenin ve HTTP isteğine deterministik idempotencyKey gönderin. Ağ zaman aşımında sunucu isteği işlemiş olabilir; yalnızca istemci tarafındaki retry sayısına güvenmek yeterli değildir.

Kotlin eğitimi sırasında Room Flow Jetpack Compose ekranında nasıl kullanılmalı?

Repository'nin Room Flow'unu ViewModel'de stateIn ile StateFlow'a dönüştürün, composable içinde `collectAsStateWithLifecycle()` kullanın. UI'da ağ yanıtını ayrı mutableStateOf alanına yazmayın. Bu yaklaşım, process death veya ekran yeniden oluşturulmasından sonra Room'daki PENDING ve FAILED durumlarının tekrar doğru çizilmesini sağlar.

Android eğitim uygulamasında offline sync için batch boyutu nasıl seçilir?

Tek bir sabit sayı seçmek yerine Perfetto ile 10, 25 ve 50 operasyon için worker süresinin p50 ve p95 değerlerini ölçün. Room tarafında IN sorgusunun bind parametresi sınırını, API tarafında gövde boyutu ve timeout sınırını kontrol edin. Her batch içindeki operasyon için ayrı idempotencyKey saklayın; aksi halde kısmi başarısızlıkta tüm batch'i güvenli yeniden denemek mümkün olmaz.

Play Store yayınlama sonrası WorkManager force-stop'tan sonra neden çalışmaz?

Kullanıcı veya test aracı `am force-stop` uyguladığında Android uygulamayı force-stopped durumuna alır ve arka plan işlerini kullanıcı uygulamayı yeniden açana kadar başlatmaz. Bunu `am kill` ile karıştırmayın: kill process death testidir, force-stop ise platformun kullanıcı niyetini temsil eden ayrı bir durumdur.

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