Geriye Doğru Yazma

Compose, bir kareyi kesin olarak sıralanmış, ileriye doğru akan üç aşamada yürütür:

İleriye doğru ilerleyen üç aşama: 1. Kompozisyon, 2. Düzen, 3. Çizim
Şekil 1. Oluşturma karesinin üç 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: Alt öğeleri ölçer ve ardından yerleştirir.
  3. Çizim: Pikselleri ekrana oluşturmak için tuval çizim komutları yayınlar.

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


Bir yazıyı "geriye doğru" yapan nedir?

Geriye doğru yazma, bir durumun okunduğu aşamadan daha sonraki bir aşamada (veya alt akış kapsamda, yani aynı oluşturma geçişinde daha sonra yürütülen bir composable kapsamda) değiştirilmesi durumunda gerçekleşir. Bu durumda Compose, daha önceki bir aşamayı veya composable'ı tekrar çalıştırılmak üzere planlayarak yeniden oluşturmaya zorlanır. 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 dönük yazma işlemlerinin sonuçları

Geriye doğru yazma işlemleri her zaman kötü bir şey değildir ve her zaman kilitlenmeleri tetiklemez ancak verimsizdir ve uygulama performansına çeşitli şekillerde zarar verebilir:

  • Ek kare oluşturma ve atlanan kareler: Geriye doğru yazma, Compose'u ardışık karelerde gereksiz oluşturma geçişleri yapmaya zorlar. Bu da CPU ve GPU kaynaklarını boşa harcar ve takılmaya neden olabilir.
  • İlk kare doğruluğu sorunları: Bileşeniniz, nihai boyutlarını veya durumunu çözmek için geriye doğru yazma gerektiriyorsa ilk kare geçersiz, varsayılan veya belirsiz verilerle (ör. sıfır boyut veya yanlış ofset) oluşturulur. Bu durum, ikinci kare oluşturulurken görsel patlamaya 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 yazarsa ekranın hiçbir zaman sabitlenmeden her kareyi sürekli olarak yeniden oluşturduğ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 boyunca ileri doğru ilerlemelidir:

Okuma aşaması Bağlam yazma Kabul edilebilir mi? Neden?
Düzen (Modifier.offset { }) Oluşturma Evet Oluşturma, durumu günceller → Düzen, yeniden oluşturmadan aynı karede daha sonra okur.
Berabere (graphicsLayer { }, drawBehind { }) Oluşturma Evet Oluşturma, durumu günceller → Çizim, son aşamada durumu okur. Oluşturma ve düzen tamamen atlanır.
Çizim Düzen Evet Düzen, durumu günceller ve çizim bunu okur. Bu geçerli bir akıştır.
Oluşturma Durum değişikliğini tetikleyen etkinlik geri çağırma (onClick, onValueChange). Not: Düzen geri çağırmaları etkinlik olarak sayılmaz. Evet Etkinlik, oluşturmayı yönlendirmek için kullanılan durumu değiştirir. Etkinlik, kare dışında (Oluşturma, 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 katmanlamasına işaret edebilir. Mümkün olduğunca bu tür URL'lerden kaçınılmalıdır.
Yerleşim (Düzende) Ölçüm (Düzende) Evet Layout'ta durumu güncelleyip yerleşimde bu durumu okumak kabul edilebilir.
Ölçüm (Düzende) Yerleşim (Düzende) Hayır - geriye doğru yazma Yerleşimde duruma yazma işlemi, daha sonra okuma durumunda olduğundan yeniden ölçüm döngüsüne neden olur.
Kompozisyon Düzen (onSizeChanged, LayoutModifier) Hayır - Geriye doğru yazma Düzen, oluşturmayı geçersiz kılar → Yeniden oluşturma döngüsü.
Kompozisyon Berabere (drawWithContent, Canvas) Hayır - Geriye doğru yazma Çizim, oluşturma → yeniden oluşturma döngüsünü geçersiz kılar.

Aşama kombinasyonları: Geriye dönük ve ileriye dönük

Aşağıda, Compose'da geriye doğru yazma örnekleri ve bunları nasıl çözebileceğiniz açıklanmıştır.

Geriye doğru: Oluşturma'da okuma, düzende yazma

  • Ne olur?: Oluşturma, hangi kullanıcı arayüzünün yayınlanacağını belirlemek için componentHeight değerini okur. Daha sonra karede, Düzen aşaması görünümleri ölçer veya yerleştirir ve componentHeight için yeni bir değer yazar (örneğin, onSizeChanged, onGloballyPositioned veya özel LayoutModifier kullanarak).
  • Sonuç: Düzen aşamasında componentHeight değiştirildiğinde, yeni tamamlanan oluşturma aşaması geçersiz kılınır. onSizeChanged öğesinin, düzen ölçümü geçişi tamamlandıktan sonra boyutunu bildirdiğini unutmayın. Güncellenen durum değeri bir sonraki geçişte sabitlenirse yeniden oluşturma işlemi bir ek kareden sonra durabilir. Ancak yeni değer boyutu değiştirmeye devam ederse sonsuz kare döngüsü oluşur. Ayrıca onGloballyPositioned, hem düzenden hem de yerleşimden sonra çalışır. Bu nedenle, içindeki durum yazma işlemleri, ardışık kareler arasında sürekli yeniden oluşturma ve yeniden düzenleme döngülerine karşı daha hassas 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)
        }
    }
}

Ters: Oluşturma'da okuma, Çizim'de yazma

  • Ne olur: Durum, Composable gövdesinde (oluşturma aşaması) okunur ancak Modifier.drawWithContent, Modifier.drawBehind veya Canvas (çizim aşaması) içinde değiştirilir.
  • Sonuç: Çizim aşaması durumu değiştirir → Oluşturma 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: Oluşturma sırasında okuma, oluşturma sırasında yazma (aynı aşama)

  • Ne olur?: count, composable işlevde okunur ve okunduktan sonra doğrudan başka bir composable içerik alanında değiştirilir count.
  • Sonuç: Anlık görüntü sistemi, aynı oluşturma geçişinde okuma ve sonraki yazma işlemlerini kaydederek mevcut kapsamı hemen 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 dönük yazma işlemlerini önlemeye yönelik temel kurallar

  1. onGloballyPositioned, onSizeChanged, onPlaced, onLayoutRectChanged veya LayoutModifier içinde, oluşturma sırasında okunan bir duruma yazmayın. Bu, ilk kare doğruluğu sorununa neden olur.
    • Düzen koordinatları veya boyutları yalnızca özel çizim için gerekliyse bunları doğrudan çizim veya düzen aşamasında okuyun (ör. Modifier.drawWithCache veya Modifier.layout kullanarak).
    • Pencere düzeyinde boyutlandırma (WindowWidthSizeClass) için: Boyut gözlemini pencere düzeyine yükseltin. Oluşturma, yerel ölçüm gerçekleşmeden önce pencere boyutu sınıflarına göre dallara ayrılır.
    • Kompozisyonu Tek Tip Tutun: Tek bir özel düzen veya FlowRow ya da LazyVerticalGrid gibi bileşenler kullanın. Bunlar, kompozisyonda kullanılan durumu yeniden oluşturmadan veya değiştirmeden 2. aşamada ölçümü ve yerleşimi ayarlar.
    • Alt oluşturma kullanın: Alt composable'ların yerel genişlik veya yüksekliğe göre dallanması gerektiğinde BoxWithConstraints veya SubcomposeLayout kullanın. Dikkatli kullanın: Alt bileşim, performans maliyetine neden olur ve genellikle önlenebilir.
    • Son çare olarak: İlk karenin yanlış olmasına izin verin ve boyutu onSizeChanged içinde depolayarak ikinci bir kare yeniden oluşturma işlemini tetikleyin. Bu durum, görünür düzen patlamasına, takılmalara ve sonsuz döngü riskine neden olur.
  2. Oluşturma işleminde ilk kez okunduktan sonra durumu değiştirmeyin:
    • Oluşturma sırasında MutableState nesnelerine SideEffect dışında güvenli bir şekilde yazabilseniz de daha önce oluşturma sırasında okumuş olabileceğiniz bir duruma yazmadığınızdan emin olmak için özel dikkat gösterin. Oluşturma işleminde durum yazma ihtiyacı ortaya çıktığında rememberUpdatedState kullanmanız önerilir. Oluşturma sırasında bir duruma başka bir şekilde yazma, genellikle eksik bir efektin veya uygunsuz şekilde tasarlanmış bir durumun ya da composable'ın işaretidir. Oluşturma iyimserdir ve her zaman bir durumun en son değeriyle yürütülür. Bu nedenle, yeniden oluşturmalarda tüm durum değişikliklerini göremeyebilirsiniz. Kullanıcı arayüzü güncellemeleri, tek seferlik etkinlikleri işlemek için kullanılmamalıdır. Bu nedenle, bir durumun değerinin yeniden oluşturma sonucunda güncellenmesi yaygın bir durum değildir.
    • Oluşturma dışında gözlemlenen durumları (ör. ViewModel alanları veya isVisible işaretleri) değiştirmekten kaçının. Oluşturmayı etkileyen durum yazma işlemleri, etkinlik lambda'larına (onClick), eş yordamlara (LaunchedEffect) veya yan etkilere (SideEffect) aittir. rememberUpdatedState, yalnızca @Composable gövdesinde kullanılan durumu değiştirmek için tasarlandığından bir istisnadır.
  3. Durum okumalarını mümkün olan en son aşamaya erteleme:
    • Durumları Draw (Modifier.graphicsLayer { alpha = ... }) veya Layout (Modifier.offset { IntOffset(...) }) aşamasında okumak, değişikliklerin yalnızca 2. veya 3. aşamayı geçersiz kılmasını ve 1. aşamayı (Oluşturma) tamamen atlamasını sağlar.