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ı.
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,ViewModelkadar yaşar — konfigürasyon değişikliklerini (döndürme, karanlık mod geçişi) atlatır ve yalnızcaViewModeltemizlendiğ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çindekirepeatOnLifecycle(Lifecycle.State.STARTED),ActivityveyaFragmentview 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 birFlowtoplarken:
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.
// İlgili okumalar
Günlükten dahası
Android In-App Review API: sıkıcı olmadan puan istemek
Google'ın In-App Review API'sine pratik bir rehber — gerçekte nasıl çalıştığı, ne zaman tetiklenmesi gerektiği ve alışılmış 'bizi puanlayın' popup'ının Play Store puanınızı sessizce nasıl zedelediği.
Android uygulama kısayolları ve Hızlı Ayarlar Karosu: uygulamayı açmadan su kaydetmek
Android'in dinamik ShortcutManager API'sine ve TileService'e pratik bir rehber — tek dokunuşlu bir eylemin uygulamayı tamamen atlamasını nasıl sağlarsınız, çoğu uygulamayı çuvallatan tuzaklarla birlikte.
Android'de 2026'da ön plan servis türleri: özelliğinize gerçekten uyanı seçmek
Android'in ön plan servis türü kısıtlamalarına pratik bir rehber — dataSync, mediaPlayback, specialUse, shortService — ve öldürülmeden ya da reddedilmeden doğru olanı nasıl seçeceğiniz.