Geriye Doğru Yazma

Oluşturma işlemi, kesinlikle sıralı ve ileriye doğru akan üç aşamada gerçekleştirilir:

Üç aşamalı bir süreç: 1. Bileşim, 2. Düzen, 3. Çizim
Şekil 1. Bir Compose çerçevesinin üç aşaması
  1. Oluşturma: Kullanıcı arayüzü ağacını oluşturmak ve güncellemek için @Composable işlevlerini çalıştırır.
  2. Düzen: Çocukları ölçer ve yerleştirir.
  3. Çizim: Pikselleri ekranda oluşturmak için tuval çizim komutları gönderir.

Herhangi bir aşamada bir Compose State okunduğunda Compose, bu durum ile ilgili aşama arasında otomatik olarak bağımlılık kaydeder.


Yazma işlemini "geriye" doğru yapan nedir?

Geriye doğru yazma, bir durum daha sonraki bir aşamada değiştirildiğinde (veya aşağı akış kapsamı içinde, yani aynı kompozisyon geçişinde daha sonra sırayla yürütülen birleştirilebilir bir kapsamda) okunduğu yerden daha sonra gerçekleşir. Bu durum, Compose'un daha önceki bir aşamayı veya birleştirilebilir öğeyi tekrar çalıştıracak şekilde planlayarak yeniden oluşturmaya zorlar. Geriye doğru yazma, optimize edilmemiş bir yeniden oluşturma döngüsüdür.

Yeniden oluşturma döngüsü aşamalarında geriye doğru yazma örneği
Şekil 2. Yeniden oluşturma döngüsü aşamalarında geriye doğru yazma örneği

Geriye doğru yazma işlemlerinin sonuçları

Geriye doğru yazma işlemi mutlaka kötü bir şey değildir ve her zaman kilitlenmeye neden olmaz. Ancak verimsizdir ve uygulama performansına çeşitli şekillerde zarar verebilir:

  • Ekstra kare oluşturma ve atlanan kareler: Geriye doğru yazma, Compose'u art arda gelen karelerde gereksiz kompozisyon geçişleri yapmaya zorlar. Bu durum, CPU ve GPU kaynaklarının boşa harcanmasına ve olası duraklamalara neden olur.
  • İlk karede doğruluk sorunları: Bileşeninizin son boyutlarını veya durumunu çözmek için geriye doğru yazma işlemi yapması gerekiyorsa ilk kare, geçersiz, varsayılan veya yerleşmemiş verilerle (ör. sıfır boyut ya da yanlış ofset) oluşturulur. Bu durum, ikinci kare oluşturulurken görsel olarak belirgin bir atlama veya düzenin yanıp sönmesine neden olur.
  • Sonsuz yeniden oluşturma döngüleri: Bir durum değişikliği düzen boyutunu değiştirirse ve düzen boyutu sürekli olarak duruma yeni bir değer geri yazarsa ekranın hiç dengelenmeden her karede sürekli olarak yeniden oluşturulduğu sonsuz bir kare döngüsü oluşturma riskiyle karşı karşıya kalabilirsiniz.

Aşamalar arasında ileriye doğru akış

Durum değişiklikleri her zaman aşamalar arasında ilerlemelidir:

Read Phase (Okuma Aşaması) Yazma Bağlamı Kabul edilebilir mi? Neden?
Düzen (Modifier.offset { }) Kompozisyon Evet Bileşen durumu günceller → Düzen, aynı çerçevede daha sonra yeniden oluşturmadan okur.
Çizim (graphicsLayer { }, drawBehind { }) Kompozisyon Evet Bileşen durumu günceller → Çizim, son aşamada durumu okur. Kompozisyon ve Düzen tamamen atlanır.
Çiz Düzen Evet Düzen durumu günceller ve çizim bunu okur. Geçerli akış.
Kompozisyon Etkinlik geri çağırması (onClick, onValueChange) durum değişikliğine neden oluyor. Not: Düzen geri çağırmaları etkinlik olarak sayılmaz. Evet Bir etkinlik, kompozisyonu yönlendirmek için kullanılan durumu değiştirir. Etkinlik, çerçeve dışında (Kompozisyon, Düzen veya Çizim'de değil) gerçekleşirse bu geçerlidir.
Kompozisyon Eş yordam (LaunchedEffect) Evet, dikkatli bir şekilde Yaşam döngüsüne/etkinliklere yanıt olarak durumu eşzamansız şekilde günceller. Efektlerden yazma işlemleri geçerli olabilir ancak verimsiz durum katmanlarına işaret edebilir. Mümkün olduğunda bunlardan kaçınılmalıdır.
Yerleşim (düzende) Ölçü (Düzende) Evet Düzen'de, durumu güncelleyip yerleşimde okumak kabul edilebilir.
Ölçü (Düzende) Yerleşim (düzende) Hayır - geriye doğru yazma Yerleştirme sırasında yazma işlemi yapılması ve ardından okuma durumuna geçilmesi, yeniden ölçüm döngüsüne neden olur.
Kompozisyon Düzen (onSizeChanged, LayoutModifier) Hayır - Tersine yazma Düzen, Bileşimi geçersiz kılıyor → Yeniden oluşturma döngüsü.
Kompozisyon Çizim (drawWithContent, Canvas) Hayır - Tersine yazma Çizim, bileşimi geçersiz kılar → Yeniden oluşturma döngüsü.

Evre kombinasyonları: Geriye dönük ve ileriye dönük

Aşağıda, Oluşturma'daki geriye doğru yazma örnekleri ve bunları nasıl çözebileceğiniz açıklanmaktadır.

Geriye doğru: Besteleme modunda okuma, Düzen modunda yazma

  • Ne olur?: Bileşen, hangi kullanıcı arayüzünün yayınlanacağını belirlemek için componentHeight öğesini okur. Çerçevenin ilerleyen kısımlarında, düzen aşamasında görünümler ölçülür veya yerleştirilir ve componentHeight için yeni bir değer yazılır (örneğin, onSizeChanged, onGloballyPositioned veya özel LayoutModifier kullanılarak).
  • Sonuç: Düzen'de componentHeight değiştirildiğinde, yeni tamamlanan Kompozisyon aşaması geçersiz kılınır. onSizeChanged raporlarının, düzen ölçümü geçişi tamamlandıktan sonra boyutlandığını unutmayın. Güncellenen durum değeri bir sonraki geçişte sabitlenirse yeniden oluşturma işlemi bir ek kare sonra durabilir. Ancak yeni değer boyutu değiştirmeye devam ederse sonsuz kare döngüsüne neden olur. Ayrıca onGloballyPositioned, hem düzen hem de yerleşimden sonra çalışır. Bu nedenle, içinde durum yazma işlemleri, ardışık karelerde sürekli yeniden oluşturma ve yeniden düzenleme döngülerine daha da yatkın hale gelir.

// ❌ BAD: Read in Composition, Written in Layout (onSizeChanged)
@Composable
fun BadAspectRatioImage(painter: Painter) {
    var calculatedHeight by remember { mutableStateOf(0.dp) }
    val density = LocalDensity.current

    // State read during COMPOSITION:
    Image(
        painter = painter,
        contentDescription = "Dynamic Image",
        modifier = Modifier
            .fillMaxWidth()
            .height(calculatedHeight)
            .onSizeChanged { size ->
                // State write during LAYOUT phase!
                // Triggers backwards write and recomposition pass
                val aspectRatio = 16f / 9f
                val widthDp = with(density) { size.width.toDp() }
                calculatedHeight = widthDp / aspectRatio
            }
    )
}

// ✅ GOOD: Measure and calculate aspect ratio height in Phase 2 (Layout) without recomposition
@Composable
fun GoodAspectRatioImage(
    painter: Painter,
    aspectRatio: Float = 16f / 9f,
    modifier: Modifier = Modifier
) {
    Layout(
        content = {
            Image(
                painter = painter,
                contentDescription = "Dynamic Image"
            )
        },
        modifier = modifier
    ) { measurables, constraints ->
        val width = constraints.maxWidth
        val height = (width / aspectRatio).toInt() // Illustrative, you can use Modifier.aspectRatio()
        val imageConstraints = constraints.copy(
            minWidth = width,
            maxWidth = width,
            minHeight = height,
            maxHeight = height
        )
        val placeable = measurables.first().measure(imageConstraints)
        layout(width, height) {
            placeable.placeRelative(0, 0)
        }
    }
}

Geriye doğru: Bestede okuma, çizimde yazma

  • Ne olur?: Durum, Composable gövdesinde (Beste aşaması) okunur ancak Modifier.drawWithContent, Modifier.drawBehind veya Canvas içinde değiştirilir (Çizim aşaması).
  • Sonuç: Çizim aşaması durumu değiştirir → Kompozisyon geçersiz kılınır → Sonsuz döngü.

// ❌ BAD: Read in Composition, Written in Draw ()
@Composable
fun BadBackwardsWriteDraw() {
    var componentHeight by remember { mutableStateOf(0.dp) }
    // State read during COMPOSITION:
    Text(
        text = "Height is: $componentHeight",
        modifier = Modifier.drawBehind {
            // State write during the DRAW phase!
            // Invalidates Composition -> triggers recomposition loop!
            componentHeight = size.height.dp
        }
    )
}

Geriye doğru: Kompozisyon okuma, kompozisyon yazma (aynı aşama)

  • Ne olur? count bir composable işlevde okunur ve okunduktan sonra doğrudan başka bir composable işlevin içerik yuvasında count değiştirilir.
  • Sonuç: Anlık görüntü sistemi, okuma ve sonraki yazma işlemlerini aynı oluşturma geçişinde kaydeder ve mevcut kapsamı anında geçersiz kılar.

// ❌ BAD: Direct write in Composable body after read
@Composable
fun BadCounter() {
    var count by remember { mutableIntStateOf(0) }
    Text("Count: $count") // State read in Composition
    Button(onClick = {}) {
        count++ // State write in Composition (Backwards write!)
    }
}
// Acceptable - but error-prone as someone may add a read before the write : Direct write in Composable body before read
@Composable
fun OkCounter() {
    var count by remember { mutableIntStateOf(0) }
    Button(onClick = {}) {
        count++ // State  write in Composition
    }
    Text("Count: $count") // State read in Composition
}


Geriye doğru yazma işlemlerini önlemeye yönelik temel kurallar

  1. Bu durum, ilk karede doğruluk sorununa neden olduğundan onGloballyPositioned, onSizeChanged veya LayoutModifier durumunda yazmayın.
    • Düzen koordinatları veya boyutları yalnızca özel çizim için gerekliyse bunları doğrudan Çizim veya Düzen aşamasında okuyun (örneğin, Modifier.drawWithCache veya Modifier.layout kullanarak).
    • Pencere düzeyinde boyutlandırma (WindowWidthSizeClass) için: Boyutlandırma gözlemini pencere düzeyine taşıyın. Yerel ölçüm gerçekleşmeden önce pencere boyutu sınıflarına göre kompozisyon dalları.
    • Besteyi Tek Tip Tutun: 2. aşamada ölçümü ve yerleşimi yeniden oluşturmadan veya beste için kullanılan durumu değiştirmeden ayarlayan tek bir özel düzen ya da FlowRow veya LazyVerticalGrid gibi bileşenler kullanın.
    • Alt kompozisyon kullanın: Alt composable'lar yerel genişliğe veya yüksekliğe göre dallanması gerektiğinde BoxWithConstraints veya SubcomposeLayout kullanın. Dikkatli olun: Alt bileşen oluşturma, performans maliyetine neden olur ve genellikle önlenebilir.
    • Son çare olarak: İkinci bir çerçeve yeniden oluşturma işlemini tetiklemek için boyutu onSizeChanged içinde saklayarak ilk çerçevenin yanlış olmasına izin verin. Bu durum, düzenin görünür şekilde atlamasına, duraklamasına ve sonsuz döngü riskine neden olur.
  2. Durumu, kompozisyonda ilk kez okunduktan sonra değiştirmeyin:
    • MutableState nesnelerine SideEffect dışında oluşturma sırasında güvenli bir şekilde yazabilirsiniz ancak daha önce oluşturma sırasında okumuş olabileceğiniz bir duruma yazmadığınızdan emin olmak için özel dikkat gösterin. Bu durumda, kompozisyonda durum yazma ihtiyacı ortaya çıktığında rememberUpdatedState kullanılması önerilir. Kompozisyon sırasında başka bir şekilde duruma yazmak genellikle eksik bir efektin veya uygunsuz şekilde tasarlanmış bir durumun ya da composable'ın işaretidir. Kompozisyonun iyimser olduğunu ve her zaman bir durumun en son değeriyle yürütüldüğünü unutmayın. Bu nedenle, yeniden oluşturma işlemlerinde tüm durum değişikliklerini görmeyebilirsiniz. Kullanıcı arayüzü güncellemeleri, tek seferlik etkinlikleri işleme yöntemi olarak kullanılmamalıdır. Bu nedenle, yeniden oluşturma sonucunda bir durumun değerinin güncellenmesi nadir görülür.
    • Bileşimin dışında gözlemlenen durumları değiştirmekten kaçının (örneğin, ViewModel alanları veya isVisible işaretleri). Durum yazma işlemlerinin, kompozisyonu etkileyen etkinlik lambdalarında (onClick), coroutine'lerde (LaunchedEffect) veya yan etkilerde (SideEffect) yer alması gerekir. rememberUpdatedState, yalnızca @Composable gövdesinde kullanılan durumu değiştirmek üzere tasarlandığı için bir istisnadır.
  3. Durum okumalarını mümkün olan en son aşamaya erteleyin:
    • Çizim (Modifier.graphicsLayer { alpha = ... }) veya Düzen (Modifier.offset { IntOffset(...) }) modundaki okuma durumları, değişikliklerin yalnızca 2. veya 3. aşamayı geçersiz kılmasını sağlar ve 1. aşama (Bileşim) tamamen atlanır.