Jetpack Compose aşamaları

Diğer kullanıcı arayüzü araç setlerinin çoğu gibi Compose da birkaç farklı aşama üzerinden bir çerçeve oluşturur. Android View sistemine baktığımızda, üç ana aşamadan yararlanabilir: ölçme, düzen ve çizim. E-posta yazma süreci çok benzerdir ancak başlangıçta kompozisyon adı verilen önemli bir ek aşama vardır.

Kompozisyon, Thinking in Composer, State ve Jetpack Compose dahil olmak üzere Compose dokümanlarımızda açıklanır.

Bir karenin üç aşaması

Oluşturma üç ana aşamadan oluşur:

  1. Beste: Ne kullanıcı arayüzü gösterilir? Compose, composable işlevleri çalıştırır ve kullanıcı arayüzünüzün bir açıklamasını oluşturur.
  2. Düzen: Kullanıcı arayüzünün yerleştirileceği nereye? Bu aşama, ölçüm ve yerleştirme olmak üzere iki adımdan oluşur. Düzen öğeleri, düzen ağacındaki her bir düğüm için kendilerini ve alt öğelerini 2D koordinatlarda ölçer ve yerleştirir.
  3. Çizim: Nasıl oluşturulur? Kullanıcı arayüzü öğeleri, genellikle bir cihaz ekranı olan tuvale çizim yapar.
Oluşturma işleminin verileri kullanıcı arayüzüne dönüştürdüğü üç aşamanın (sıra, veri, kompozisyon, düzen, çizim, kullanıcı arayüzü) görüntüsü.
Şekil 1. Oluşturma işleminin verileri kullanıcı arayüzüne dönüştürdüğü üç aşama.

Bu aşamaların sırası genellikle aynıdır. Böylece, bir çerçeve oluşturmak için verilerin bileşimden düzene ve çizime tek bir yönde akmasına izin verilir (tek yönlü veri akışı olarak da bilinir). BoxWithConstraints ile LazyColumn ve LazyRow alt öğelerinin bile üst öğenin düzen aşamasına bağlı olduğu önemli istisnalardır.

Bu üç aşamanın neredeyse her karede gerçekleştiğini varsayabilirsiniz. Ancak Compose performans olsun diye tüm bu aşamalarda aynı girişlerden elde edilen aynı sonuçları hesaplamak için tekrar eden işlerden kaçınır. Compose, eski bir sonucu yeniden kullanabiliyorsa bir composable işlevini çalıştıran atlar ve Compose kullanıcı arayüzü, zorunlu olmadığında ağacın tamamını yeniden düzenlemez veya yeniden çizmez. Compose yalnızca kullanıcı arayüzünü güncellemek için gereken minimum miktarda işlemi gerçekleştirir. Oluşturma işlemi farklı aşamalardaki durum okumalarını izlediği için bu optimizasyon mümkündür.

Aşamaları anlama

Bu bölümde, composable'lar için üç Oluşturma aşamasının nasıl yürütüldüğü daha ayrıntılı olarak açıklanmaktadır.

Beste

Oluşturma aşamasında, Compose çalışma zamanı composable işlevleri yürütür ve kullanıcı arayüzünüzü temsil eden bir ağaç yapısı oluşturur. Bu kullanıcı arayüzü ağacı, aşağıdaki videoda gösterildiği gibi, sonraki aşamalar için gerekli tüm bilgileri içeren düzen düğümlerinden oluşur:

2. Şekil. Kullanıcı arayüzünüzü temsil eden ve bileşim aşamasında oluşturulan ağaç.

Kodun ve kullanıcı arayüzü ağacının alt bölümü aşağıdaki gibi görünür:

Beş composable'dan oluşan bir kod snippet'i ve ortaya çıkan kullanıcı arayüzü ağacı, alt düğümleri üst düğümlerinden ayrılır.
Şekil 3. İlgili kodun yer aldığı bir kullanıcı arayüzü ağacı alt bölümü.

Bu örneklerde, koddaki her composable işlev, kullanıcı arayüzü ağacında tek bir düzen düğümüyle eşlenir. Daha karmaşık örneklerde composable'lar mantık ve kontrol akışı içerebilir ve farklı durumlarda farklı bir ağaç üretebilir.

Düzen

Compose, düzen aşamasında giriş olarak oluşturma aşamasında oluşturulan kullanıcı arayüzü ağacını kullanır. Düzen düğümleri koleksiyonu, her bir düğümün 2D uzayda boyutuna ve konumuna karar vermek için gereken tüm bilgileri içerir.

4. Şekil. Düzen aşamasında kullanıcı arayüzü ağacındaki her düzen düğümünün ölçümü ve yerleşimi.

Düzen aşamasında ağaçta geçiş için aşağıdaki üç adımlı algoritma kullanılır:

  1. Alt öğeleri ölçme: Düğüm, varsa alt öğelerini ölçer.
  2. Kendi boyutuna karar verme: Düğüm, bu ölçümlere göre kendi boyutuna karar verir.
  3. Alt öğeleri yerleştir: Her alt düğüm, düğümün kendi konumuna göre yerleştirilir.

Bu aşamanın sonunda her düzen düğümünde:

  • Atanmış bir width ve height
  • Çizilmesi gereken x, y koordinatı

Önceki bölümde yer alan kullanıcı arayüzü ağacını hatırlayın:

Beş composable'dan oluşan bir kod snippet'i ve elde edilen kullanıcı arayüzü ağacında, alt düğümler üst düğümlerinden ayrılıyor

Bu ağaç için algoritma şu şekilde çalışır:

  1. Row alt öğelerini (Image ve Column) ölçer.
  2. Image ölçülür. Hiç alt öğesi olmadığından kendi boyutunu belirler ve boyutu Row'a geri bildirir.
  3. Bundan sonra Column ölçülür. Önce kendi alt öğelerini (iki Text composable) ölçer.
  4. İlk Text ölçülür. Hiç alt öğesi olmadığından kendi boyutuna karar verir ve boyutunu Column'e geri bildirir.
    1. İkinci Text ölçülür. Hiç alt öğesi olmadığından kendi boyutuna karar verir ve Column ekibine bildirir.
  5. Column, kendi boyutunu belirlemek için alt ölçümleri kullanır. Maksimum alt öğe genişliği ve alt öğelerinin boylarının toplamını kullanır.
  6. Column, alt öğelerini dikey olarak birbirlerinin altına yerleştirir.
  7. Row, kendi boyutunu belirlemek için alt ölçümleri kullanır. Maksimum alt öğe boyunu ve alt öğelerinin genişliklerinin toplamını kullanır. Daha sonra çocuklarını yerleştiriyor.

Her düğümün yalnızca bir kez ziyaret edildiğini unutmayın. Compose çalışma zamanı, tüm düğümleri ölçmek ve yerleştirmek için kullanıcı arayüzü ağacından yalnızca bir geçiş gerektirir. Bu da performansı artırır. Ağaçtaki düğüm sayısı arttıkça ağaçta geçiş için harcanan süre doğrusal bir düzende artar. Buna karşılık, her bir düğüm birden fazla kez ziyaret edildiyse geçiş süresi katlanarak artar.

Çizim

Çizim aşamasında, ağaç tekrar yukarıdan aşağıya doğru taşınır ve her düğüm ekranda sırayla kendini çizer.

5. Şekil. Çizim aşamasında pikseller çizilir.

Önceki örneğe göre, ağaç içeriği aşağıdaki şekilde çizilir:

  1. Row, arka plan rengi gibi sahip olabileceği tüm içeriği çizer.
  2. Image kendini gösterir.
  3. Column kendini gösterir.
  4. Sırasıyla birinci ve ikinci Text kendilerini çizer.

6. Şekil. Kullanıcı arayüzü ağacı ve çizilmiş gösterimi.

Durum okumaları

Yukarıda listelenen aşamalardan birinde anlık görüntü durumunun değerini okuduğunuzda Compose değer okunurken ne yapmakta olduğunu otomatik olarak izler. Bu izleme, durum değeri değiştiğinde Compose'un okuyucuyu yeniden çalıştırmasına olanak tanır ve Compose'da durum gözlemlenebilirliğinin temelini oluşturur.

Eyalet, genellikle mutableStateOf() kullanılarak oluşturulur ve ardından value mülküne doğrudan erişerek veya Kotlin mülk yetkilendirmesi kullanarak iki yoldan biriyle erişilir. State incomposables bölümünde bu konu hakkında daha fazla bilgi edinebilirsiniz. Bu kılavuzun amaçları doğrultusunda "durum okuma", bu eşdeğer erişim yöntemlerinden herhangi birini ifade eder.

// State read without property delegate.
val paddingState: MutableState<Dp> = remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.padding(paddingState.value)
)

// State read with property delegate.
var padding: Dp by remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.padding(padding)
)

Mülk yetkisi altında, Eyalet'in value değerine erişmek ve bu kodu güncellemek için "getter" ve "setter" işlevleri kullanılır. Bu getter ve setter işlevleri, yalnızca mülke değer olarak referans verildiğinde çağrılır, oluşturulduğunda değil. Bu nedenle, yukarıdaki iki yöntem eşdeğerdir.

Okuma durumu değiştiğinde yeniden yürütülebilen her kod bloğu bir yeniden başlatma kapsamıdır. Oluşturma işlemi, durum değeri değişikliklerini takip eder ve farklı aşamalarda kapsamları yeniden başlatır.

Aşamalı durum okumaları

Yukarıda belirtildiği gibi, Oluşturma'da üç ana aşama vardır ve Oluştur, her bir aşamada hangi durumun okunduğunu izler. Bu, Compose'un yalnızca kullanıcı arayüzünüzde etkilenen her bir öğe için çalışması gereken belirli aşamaları bildirmesine olanak tanır.

Her aşamayı gözden geçirelim ve içinde bir durum değeri okunduğunda neler olduğunu açıklayalım.

1. Aşama: Kompozisyon

Bir @Composable işlevindeki veya lambda bloğundaki durum okumaları bileşimi ve potansiyel olarak sonraki aşamaları etkiler. Durum değeri değiştiğinde, toplayıcı bu durum değerini okuyan tüm composable işlevleri yeniden çalıştırır. Girişler değişmediyse çalışma zamanının, composable işlevlerin bazılarını veya tümünü atlamaya karar verebileceğini unutmayın. Daha fazla bilgi için Girdiler değişmediyse atlama bölümüne bakın.

Oluşturma sonucuna bağlı olarak, Compose kullanıcı arayüzü düzen ve çizim aşamalarını çalıştırır. İçerik aynı kalırsa ve boyut ile düzen değişmezse bu aşamaları atlayabilir.

var padding by remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    // The `padding` state is read in the composition phase
    // when the modifier is constructed.
    // Changes in `padding` will invoke recomposition.
    modifier = Modifier.padding(padding)
)

2. Aşama: Düzen

Düzen aşaması iki adımdan oluşur: ölçüm ve yerleşim. Ölçüm adımı, Layout composable'a iletilen lambda ölçümünü, LayoutModifier arayüzünün MeasureScope.measure yöntemini vb. çalıştırır. Yerleşim adımı, layout işlevinin yerleşim bloğunu, Modifier.offset { … } öğesinin lambda bloğunu ve benzer öğeleri çalıştırır.

Bu adımların her biri sırasındaki durum okumaları, düzeni ve potansiyel olarak çizim aşamasını etkiler. Durum değeri değiştiğinde, Compose kullanıcı arayüzü düzen aşamasını planlar. Ayrıca, boyut veya konum değiştiyse çizim aşamasını da çalıştırır.

Daha kesin konuşmak gerekirse ölçüm adımı ve yerleşim adımı ayrı yeniden başlatma kapsamlarına sahiptir. Yani yerleşim adımında durum okumaları bundan önceki ölçüm adımını tekrar çağırmaz. Ancak bu iki adım genellikle iç içe geçmiştir. Bu yüzden, yerleşim adımında okunan bir durum, ölçüm adımına ait diğer yeniden başlatma kapsamlarını etkileyebilir.

var offsetX by remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.offset {
        // The `offsetX` state is read in the placement step
        // of the layout phase when the offset is calculated.
        // Changes in `offsetX` restart the layout.
        IntOffset(offsetX.roundToPx(), 0)
    }
)

3. Aşama: Çizim

Çizim kodu sırasındaki durum okumaları çizim aşamasını etkiler. Yaygın örnekler arasında Canvas(), Modifier.drawBehind ve Modifier.drawWithContent yer alır. Durum değeri değiştiğinde, Compose kullanıcı arayüzü yalnızca çizim aşamasını çalıştırır.

var color by remember { mutableStateOf(Color.Red) }
Canvas(modifier = modifier) {
    // The `color` state is read in the drawing phase
    // when the canvas is rendered.
    // Changes in `color` restart the drawing.
    drawRect(color)
}

Durum okumaları optimize ediliyor

Compose yerelleştirilmiş durum okuma izlemesi gerçekleştirirken her bir durumu uygun bir aşamada okuyarak yapılan iş miktarını en aza indirebiliriz.

Şimdi bir örnek inceleyelim. Burada, son düzen konumunu dengelemek için ofset değiştiriciyi kullanan ve kullanıcı sayfayı kaydırırken paralaks etkisine neden olan bir Image() örneğimiz var.

Box {
    val listState = rememberLazyListState()

    Image(
        // ...
        // Non-optimal implementation!
        Modifier.offset(
            with(LocalDensity.current) {
                // State read of firstVisibleItemScrollOffset in composition
                (listState.firstVisibleItemScrollOffset / 2).toDp()
            }
        )
    )

    LazyColumn(state = listState) {
        // ...
    }
}

Bu kod çalışır, ancak optimum olmayan performansa neden olur. Kod, yazıldığı gibi firstVisibleItemScrollOffset durumunun değerini okur ve Modifier.offset(offset: Dp) işlevine iletir. Kullanıcı sayfayı kaydırdıkça firstVisibleItemScrollOffset değeri değişir. Bildiğimiz gibi Compose, tüm durum okumalarını izler. Böylece okuma kodu yeniden başlatılabilir (yeniden çağırma). Örneğimizde bu, Box içeriğinin içeriğidir.

Bu, bileşim aşamasında okunan bir durum örneğidir. Bu aslında kötü bir şey değildir ve aslında veri değişikliklerinin yeni kullanıcı arayüzü oluşumuna izin veren yeniden düzenlemenin temelini oluşturur.

Bu örnekte, her kaydırma etkinliği, composable'ın tamamının yeniden değerlendirilmesi ve ardından ölçülmesi, düzenlenip son olarak çizilmesiyle sonuçlanacağı için bu yöntem optimum değildir. Gösterdiğimiz öğe değişmemiş olsa da, yalnızca gösterildiği yerde değişiklik olsa da her kaydırmada Oluşturma aşamasını tetikliyoruz. Durum okumamızı, yalnızca düzen aşamasını yeniden tetikleyecek şekilde optimize edebiliriz.

Belirli uzaklıkta kopyasının başka bir sürümünü kullanabilirsiniz: Modifier.offset(offset: Density.() -> IntOffset).

Bu sürüm, lambda parametresini alır. Bu parametre, elde edilen ofset lambda bloğu tarafından döndürülür. Şimdi kodumuzu kullanmak için güncelleyelim:

Box {
    val listState = rememberLazyListState()

    Image(
        // ...
        Modifier.offset {
            // State read of firstVisibleItemScrollOffset in Layout
            IntOffset(x = 0, y = listState.firstVisibleItemScrollOffset / 2)
        }
    )

    LazyColumn(state = listState) {
        // ...
    }
}

Peki, bu neden daha yüksek performans sağlar? Değiştiriciye sağladığımız lambda bloğu, düzen aşamasında (özellikle düzen aşamasının yerleştirme adımı sırasında) çağrılır. Diğer bir deyişle, firstVisibleItemScrollOffset durumumuz beste sırasında artık okunmaz. Oluşturma işlemi durum okunduğunda izlediği için bu değişiklik, firstVisibleItemScrollOffset değeri değişirse Oluşturma özelliğinin yalnızca düzen ve çizim aşamalarını yeniden başlatması gerektiği anlamına gelir.

Bu örnek, sonuçta ortaya çıkan kodu optimize edebilmek için farklı ofset değiştiricilerine dayanır, ancak genel yaklaşım doğrudur: Durum okumalarını mümkün olan en düşük aşamaya göre yerelleştirmeye çalışın. Böylece, Compose en az miktarda işi gerçekleştirebilir.

Elbette, çoğu zaman bileşim aşamasında durumların okunması kesinlikle gereklidir. Yine de durum değişikliklerini filtreleyerek yeniden oluşturma sayısını en aza indirebileceğimiz durumlar vardır. Bu konu hakkında daha fazla bilgi için derivedStateOf: bir veya birden fazla durum nesnesini başka bir duruma dönüştürme bölümüne bakın.

Yeniden oluşturma döngüsü (döngüsel faz bağımlılığı)

Daha önce, Oluşturma aşamalarının her zaman aynı sırayla çağrıldığından ve aynı kare içindeyken geri gitmenin mümkün olmadığını belirtmiştik. Ancak bu durum, uygulamaların farklı kareler arasında bileşim döngülerine girmesini engellemez. Aşağıdaki örneğe bakın:

Box {
    var imageHeightPx by remember { mutableStateOf(0) }

    Image(
        painter = painterResource(R.drawable.rectangle),
        contentDescription = "I'm above the text",
        modifier = Modifier
            .fillMaxWidth()
            .onSizeChanged { size ->
                // Don't do this
                imageHeightPx = size.height
            }
    )

    Text(
        text = "I'm below the image",
        modifier = Modifier.padding(
            top = with(LocalDensity.current) { imageHeightPx.toDp() }
        )
    )
}

Burada, resim üstte, sonra da altında metin olacak şekilde dikey bir sütun uyguladık (kötücül şekilde). Resmin çözümlenen boyutunu bilmek için Modifier.onSizeChanged() kullanıyoruz, ardından metni aşağı kaydırmak için de Modifier.padding() kullanıyoruz. Px dönüşümden Dp değerine geri yapılan doğal olmayan dönüşüm, zaten kodda bir sorun olduğunu gösterir.

Bu örnekteki sorun, "nihai" düzene tek bir kare içinde ulaşmamamızdır. Kod, birden fazla karenin oluşmasına dayanır. Bu da gereksiz işler yapar ve kullanıcı arayüzü, kullanıcının ekranda birden atlamasına neden olur.

Neler olduğunu görmek için her bir kareyi adım adım inceleyelim:

İlk karenin beste aşamasında imageHeightPx 0 değerine sahiptir ve metin Modifier.padding(top = 0) ile sağlanır. Ardından, düzen aşaması izler ve onSizeChanged değiştiricisi için geri çağırma çağrılır. Bu aşamada imageHeightPx, resmin gerçek yüksekliğine güncellenir. Sonraki kare için yeniden kompozisyon planlamaları oluşturun. Çizim aşamasında, değer değişikliği henüz yansıtılmadığından metin 0 dolgusuyla oluşturulur.

Ardından, oluşturma işlemi imageHeightPx değer değişikliğiyle programlanan ikinci kareyi başlatır. Durum, Box içerik bloğunda okunur ve beste aşamasında çağrılır. Bu kez metne, resim yüksekliğiyle eşleşen bir dolgu sağlanır. Düzen aşamasında kod, imageHeightPx değerini tekrar ayarlar ancak değer aynı kaldığı için yeniden oluşturma planlanmaz.

Sonunda, metinde istenen dolguyu elde ederiz, ancak dolgu değerini farklı bir aşamaya geri aktarmak için fazladan bir kare harcamak uygun değildir ve bunun sonucunda çakışan içeriğe sahip bir çerçeve oluşturulur.

Bu örnek sahte görünebilir, ancak şu genel düzene dikkat edin:

  • Modifier.onSizeChanged(), onGloballyPositioned() veya diğer bazı düzen işlemleri
  • Bazı eyaletleri güncelle
  • Bu durumu bir düzen değiştiriciye (padding(), height() veya benzeri) giriş olarak kullanın
  • Tekrarlanabilir

Yukarıdaki örneğin çözümü, uygun düzen temel öğelerinin kullanılmasıdır. Yukarıdaki örnek basit bir Column() ile uygulanabilir ancak özel bir düzen yazma gerektiren daha karmaşık bir örneğiniz olabilir. Daha fazla bilgi için Özel düzenler kılavuzuna bakın.

Buradaki genel ilke, birden fazla kullanıcı arayüzü öğesi için ölçülmesi ve birbiriyle ilişkili olarak yerleştirilmesi gereken tek bir doğruluk kaynağına sahip olmaktır. Uygun bir temel düzenin kullanılması veya özel bir düzenin oluşturulması, minimum paylaşılan üst öğenin birden fazla öğe arasındaki ilişkiyi koordine edebilecek bilgi kaynağı olarak işlev gördüğü anlamına gelir. Dinamik durum uygulamak bu ilkeye aykırıdır.