Jetpack Compose'da recomposition: gerçekte neyin tetiklediği ve tahmin etmeyi bırakışım
Jetpack Compose recomposition'a 2026 için pratik bir rehber — tip kararlılığı, kararsız lambda'lar, LazyColumn anahtarları ve derleyicinin kendi metrik raporunu okumak.
Takılan bir Compose ekranının neredeyse hiçbir zaman açık bir nedeni yoktur. Ne bir stack trace, ne bir çökme, ne de bir lint uyarısı vardır — sadece olması gerekenden bir tık yavaş hissettiren bir liste, ya da her tuş vuruşunun yarım kare gerisinde kalan bir metin alanı. Genellikle sebep, recomposition’ın gerekenden fazla UI ağacı üzerinde çalışmasıdır, ve can sıkıcı olan kısım Compose’un bu olduğunda size haber vermemesidir. Sessizce, siz gidip aramaya başlayana kadar, her karede gereğinden fazla iş yapar. İşte bir composable’ın atlanıp atlanmayacağını ya da yeniden çalıştırılacağını gerçekte neyin belirlediği ve tahmin etmenin yerini alan iki araç.
Recomposition gerçekte nedir
Compose, değişkenleri değil state okumalarını takip eder. Bir composable fonksiyon bir State<T>’i okuduğunda — doğrudan ya da remember { mutableStateOf(...) } üzerinden — runtime bu okumayı geçerli composition scope’una karşı kaydeder. State’in değeri değiştiğinde, yalnızca onu gerçekten okuyan scope’lar yeniden çalışacak şekilde zamanlanır. Ağaçtaki geri kalan her şey — teoride — dokunulmadan bırakılır. Bu cümlede işi yapan sözler “onu gerçekten okuyan scope’lar”dır ve bu scope’ların hangileri olduğunu bulmak işlerin ters gittiği yerdir.
Derleyicinin gerçek sorusu: bu tip kararlı mı
Her composable fonksiyonun parametreleri Compose derleyicisi tarafından kararlı ya da kararsız olarak sınıflandırılır. Kararlı bir tip, derleyicinin Compose’a haber verilmeden değişmeyeceğini kanıtlayabildiği bir tiptir — yani değer son seferkiyle equals() açısından eşitse, Compose fonksiyonu yeniden çalıştırmayı tamamen atlar. Kararsız bir tipe bu kadar güvenilemez, bu yüzden onu kullanan composable, ebeveyninin her recomposition’ında yeniden çalışır — gerçekte okuduğu bir şey değişmiş olsun ya da olmasın.
// Kararlı: her property val, ve String/Int tanım gereği kararlı.
data class PantryItem(val id: Long, val name: String, val quantity: Int)
// Kararsız: bir var property, derleyicinin bu instance'ın bir composable'ın
// referans tuttuğu sırada mutasyona uğramayacağını kanıtlayamaması demektir.
data class PantryItemDraft(var name: String, var quantity: Int)
var versiyonu bir stil takıntısı değil — Compose’un atlayabildiği bir composable ile atlayamadığı bir composable arasındaki farktır. Aynı sorun düz List<T> ve Map<K, V> parametrelerinde de ortaya çıkar: derleyici List arayüzünü kararsız kabul eder, çünkü hiçbir şey çağıranın ona bir MutableList verip sonra mutasyona uğratmasını engellemez. Kararlı val alanlarla dolu bir data class, düz bir List içine sarıldığında yine de kararsız bir parametredir, ve bu, gerçek bir kod tabanında kararlılığın sessizce bozulduğu en yaygın tek noktadır.
Fark edilmeden nereye sızıyor
Kendi ekranlarımda izini sürdüğüm gereksiz recomposition’ların çoğu iki kalıba bağlanıyor:
Hoisted edilmemiş lambda’lar. Bir composable gövdesinin içinde satır içi tanımlanan bir lambda, yakaladığı değerler değişmemiş olsa bile, ebeveynin her recomposition’ında yeni bir nesnedir. Bir çocuk composable’a aşağı geçirildiğinde, bu yeni instance eşitlik kontrolünü geçemez ve çocuğu, gerçekte okuduğu bir şey değişmiş olsun ya da olmasın recompose olmaya zorlar.
// PantryScreen her recompose olduğunda yeniden yaratılır — ItemRow'da
// atlamayı boşa çıkarır.
PantryList(items = items, onDelete = { id -> viewModel.delete(id) })
// Bir kez hoisted edildi, PantryScreen'in recomposition'ları boyunca kararlı.
val onDelete = remember(viewModel) { { id: Long -> viewModel.delete(id) } }
PantryList(items = items, onDelete = onDelete)
Bir repository’den doğrudan geçirilen koleksiyonlar. collectAsStateWithLifecycle() ile toplanan bir Flow<List<PantryItem>>, içerik aynı olsa bile her emisyonda size taze bir List instance’ı verir — bu, Room sorgusunun ilgisiz bir tablo yazımı sonrası yeniden çalıştığı hemen ardından sık görülür. Bu liste birden fazla çocuk composable’a geçiriliyorsa, hepsi her emisyonda recompose olur. Asıl önemsediğiniz şeyin ham liste değil hesaplanmış bir değer olduğu durumlarda, okuma tarafını derivedStateOf içine sarmak bunu düzeltir:
val isEmpty by remember {
derivedStateOf { items.isEmpty() }
}
derivedStateOf yalnızca hesaplanmış çıktısı değiştiğinde recomposition tetikler, girdileri değiştiğinde değil — yani 40 öğeden 41’e çıkan bir liste, yalnızca listenin boş olup olmadığını önemseyen bir composable’ı yeniden tetiklemez.
Listeler: satır başına recomposition’ı sınırlayan anahtar
LazyColumn, kimlik anahtarı olarak varsayılan olarak öğe konumunu kullanır. Sonda değil de herhangi bir yerde yeniden sıralama, ekleme ya da silme yapın, o indeksten sonraki her satır “değişmiş” sayılır — çünkü Compose’un takip ettiği şey içerik değil konumdur. Açık, kararlı bir anahtar vermek, recomposition’ı içeriği gerçekten değişen satırlarla sınırlayarak bunu düzeltir:
LazyColumn {
items(items = pantryItems, key = { it.id }) { item ->
PantryItemRow(item)
}
}
Bu, birkaç yüz satırı barkod taramalarından ve fiş içe aktarmalarından düzenli olarak barındıran Stocky’nin kiler listesinin arkasındaki tam kalıp — kararlı bir anahtar olmadan, bir öğenin miktarını düzenlemek, değişen tek satır yerine görünür listenin tamamını recompose ederdi.
Derleyicinin kendi raporunu okumak
Hangi composable’ların kararsız olduğunu tahmin etmeye gerek yok — Compose derleyicisi size doğrudan söyleyebilir. Gradle build’ine bir metrik flag’i eklemek, her composable fonksiyonu, atlanabilir olup olmadığını ve tam olarak hangi parametrenin onu kararsız yaptığını listeleyen bir rapor üretir:
// build.gradle.kts
composeCompiler {
metricsDestination = layout.buildDirectory.dir("compose_metrics")
reportsDestination = layout.buildDirectory.dir("compose_metrics")
}
Üretilen *-composables.txt raporu, işaretlediği her fonksiyonda kararsız parametreyi ismiyle belirtir; bu da “bu ekran neden yavaş” sorusunu bir profiling oturumundan bir grep’e dönüştürür. Bunu, bir liste ya da sık güncellenen bir değer içeren her ekranı göndermeden önce çalıştırıyorum — ucuz, kesin ve data class içindeki var hatasını dosyayı açmak kadar kısa sürede yakalıyor.
Sonuç
Recomposition doğası gereği bir performans sorunu değil — Compose, fonksiyonları ucuza ve sık sık yeniden çalıştıracak şekilde tasarlandı. Sorun sınırlandırılmamış recomposition’dır: bir kararsız parametre, bir hoisted edilmemiş lambda ya da eksik bir liste anahtarı derleyiciye bir şeyi atlamanın güvenli olduğunu kanıtlayamayacağını söylediği için tüm bir ekranın yeniden çalışması. Bu üç şeyi düzeltin — val alanlarla kararlı tipler, hoisted edilmiş lambda’lar ve her LazyColumn öğesinde açık anahtarlar — ve Compose performans sorunu gibi görünen çoğu şeyin zaten çözülmüş olduğu ortaya çıkar.
// İ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.