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

2026'da Android'de yapılandırılmış eşzamanlılık: scope'lar, iptal ve önlediği sızıntılar

Android'de Kotlin yapılandırılmış eşzamanlılığına pratik bir rehber — viewModelScope'a karşı lifecycleScope, iptalin neden yayıldığı ve sessizce önlediği coroutine sızıntıları.

MFKAPPS 4 dk okuma

Android’de ayıkladığım coroutine hatalarının çoğu suspend fonksiyonlarının yanlış bir şey yapmasıyla ilgili değildi — başlatan şeyden daha uzun yaşayan bir coroutine ile ilgiliydi. Bir ağ çağrısı, ekranı kapandıktan sonra biter ve yok edilmiş bir view’ı güncellemeye çalışırken çöker. Bir Flow collector’ı, kimsenin bakmadığı bir ekran için pil tüketerek arka planda çalışmaya devam eder. Yapılandırılmış eşzamanlılık, bunları temizlemeyi hatırlayarak değil, yapı gereği ortadan kaldıran disiplindir — ve gerçekte neyi garanti ettiğini anlamak, hangi Scope’un nerede kullanılacağını ezberlemekten daha değerlidir.

Temel fikir: bir coroutine kendi scope’undan daha uzun yaşayamaz

Kotlin’deki her coroutine bir CoroutineScope’a aittir ve o scope onun yaşam süresini tanımlar. Bir scope içinde bir coroutine başlattığınızda, o scope’un job’ının bir çocuğu olur. Scope’u iptal edin, ne kadar derine yerleşmiş olursa olsun, kaç tane async çağrısına dağılmış olursa olsun, her çocuk onunla birlikte iptal edilir. Garanti tamamen bu: bir coroutine’i, ona sahip olan scope’un dışına yanlışlıkla sızdıramazsınız, çünkü dil size bunun bir yolunu vermez.

class BudgetViewModel(private val repo: TransactionRepository) : ViewModel() {
    fun refreshMonthlyTotal() {
        viewModelScope.launch {
            val total = repo.computeMonthlyTotal() // burada suspend olur
            _monthlyTotal.value = total
        }
    }
}

Kullanıcı başka bir yere geçerse ve ViewModel, computeMonthlyTotal() ortasında temizlenirse, viewModelScope otomatik olarak iptal edilir ve yukarıdaki coroutine durur — _monthlyTotal’a yazan satıra asla ulaşmaz. Kontrol edilecek manuel bir bayrak yok, yazmadan önce bir isActive koruması gerekmiyor, çünkü coroutine iptalden sonra bir askıya alma noktasına ulaştığında basitçe devam etmiyor.

viewModelScope, lifecycleScope ve doğrusunu seçmek

Android, bileşen yaşam sürelerine bağlı iki scope sunar ve yanlışını seçmek, gördüğüm en yaygın yapılandırılmış eşzamanlılık hatasıdır:

  • viewModelScope, ViewModel kadar yaşar — konfigürasyon değişikliklerini (döndürme, karanlık mod geçişi) atlatır ve yalnızca ViewModel temizlendiğinde, tipik olarak kullanıcı kalıcı olarak başka yere geçtiğinde iptal edilir. Sonucunu ekranın bir döndürmeden sonra hâlâ önemsediği işler için kullanın: veri yükleme, bir repository’ye yazma, türetilmiş bir değer hesaplama.
  • lifecycleScope, özellikle de içindeki repeatOnLifecycle(Lifecycle.State.STARTED), Activity veya Fragment view yaşam süresine bağlıdır. Ekran görünür olmadığında gerçekten durması gereken her şey için kullanın — en yaygın olarak, UI’ı güncellemek için bir Flow toplarken:
class ReminderListFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.reminders.collect { reminders ->
                    adapter.submitList(reminders)
                }
            }
        }
    }
}

Bunun yerine doğrudan viewModelScope içinde toplamak, uygulama arka plandayken collector’ı — ve tetiklediği her Room sorgusunu — çalışır durumda tutardı, çünkü viewModelScope view’ın gittiğini bilmez. Bu bir çökme anlamında sızıntı değil; daha sessiz, daha yavaş bir bedel: boşa harcanan CPU, boşa harcanan pil ve kimsenin göremediği bir UI’a yeniden yayın yapan bir Flow. repeatOnLifecycle tam olarak bu yüzden çözümdür — çünkü STARTED’a bağlı taze bir scope yeniden türetir, yaşam döngüsü o durumun içine ve dışına hareket ettikçe toplamayı iptal edip yeniden başlatır.

Yapılandırılmış eşzamanlılık hata yönetimini dürüst kılar

Yapılandırılmış eşzamanlılıktan önce, yaygın bir kalıp GlobalScope.launch ile çıplak bir coroutine fırlatmaktı — hiçbir üst tarafından denetlenmeyen, scope’suz ve sahipsiz. Eğer hata fırlatırsa istisna hiçbir yararlı yere gitmezdi, ve bittiğinde ekran çoktan gitmişse çöküşü yakalayacak hiçbir şey kalmazdı. Yapılandırılmış eşzamanlılık her coroutine’in bir üst sahip olmasını zorunlu kılar, bu da her istisnanın yayılacağı tanımlı bir yer olduğu anlamına gelir: bir CoroutineExceptionHandler kuran her şeye kadar job hiyerarşisi boyunca yukarı, ya da hiçbiri kurmadıysa scope’un kendi iptaline.

private val handler = CoroutineExceptionHandler { _, throwable ->
    _uiState.value = UiState.Error(throwable.message)
}

fun syncPantryFromReceipt(bitmap: Bitmap) {
    viewModelScope.launch(handler) {
        val items = ocrEngine.extract(bitmap) // hata fırlatabilir
        repo.insertAll(items)
    }
}

Bu, Stocky’nin başarısız bir fiş taramasını nasıl ele aldığının arkasındaki kalıptır — OCR adımı bir düzine nedenden dolayı hata fırlatabilir (kötü aydınlatma, okunamayan bir yazı tipi, hiç görmediği bir fiş formatı), ve yapılandırılmış eşzamanlılık bu hatanın sessizce fırlat-ve-unut bir coroutine içinde kaybolmak yerine tam olarak tek bir yere yüzeye çıkmasını garanti eder.

async/await: iptal iki yönde de işler

coroutineScope { } ve async builder’ları aynı garantiyi kardeş coroutine’lere de genişletir: bir coroutineScope’un bir çocuğu hata fırlatırsa, diğer tüm çocuklar da iptal edilir, ve istisna ancak hepsi durduktan sonra dışarı yayılır. Bu, yalnızca birlikte anlam ifade eden eşzamanlı işi dağıttığınız her durumda önemlidir:

suspend fun loadDashboard(): DashboardState = coroutineScope {
    val totalDeferred = async { repo.computeMonthlyTotal() }
    val trendDeferred = async { repo.computeSpendingTrend() }
    DashboardState(totalDeferred.await(), trendDeferred.await())
}

computeSpendingTrend() hata fırlatırsa, computeMonthlyTotal() de iptal edilir — kısmen başarısız olmuş bir istekten sessizce render edilmiş yarım bir gösterge paneliyle asla baş başa kalmazsınız. Yapılandırılmış eşzamanlılık olmadan, o ikinci async ilki çöktükten sonra da çalışmaya devam ederdi, sonucu kimse tarafından beklenmeden, işi boşa gitmiş olurdu.

Sonuç

Yapılandırılmış eşzamanlılık bir stil tercihi değil — “bunu iptal etmeyi unuttum mu”yu bir çalışma zamanı hatasından derleme zamanı imkânsızlığına dönüştüren şeydir. Pratikte önemli olan kural şu: yakında olduğu için değil, sonucun ne kadar süre gerekeceğiyle eşleşen scope içinde başlatın. Döndürmeden sonra bile ekranın hâlâ önemsediği işler için viewModelScope, view görünür olmadığında durması gereken her şey için repeatOnLifecycle ile lifecycleScope, ve bir hatanın hiçbir yere değil tanımlı bir yere gitmesi gerektiği her yerde bir CoroutineExceptionHandler.