Jetpack Compose aşamaları

Diğer kullanıcı arayüzü araç setlerinin çoğunda olduğu gibi, Compose da bir kareyi birkaç farklı aşamada oluşturur. Android Görüntüleme sistemine baktığımızda üç ana aşama olduğunu görürüz: ölçüm, düzen ve çizim. Oluşturma işlemine çok benzer ancak başlangıçta oluşturma adı verilen önemli bir ek aşaması vardır.

Kompozisyon, Compose'da Düşünme ve Durum ve Jetpack Compose gibi Compose dokümanlarımızda açıklanmaktadır.

Bir karenin üç aşaması

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

  1. Kompozisyon: Gösterilecek kullanıcı arayüzü. Oluştur, composable işlevleri çalıştırır ve kullanıcı arayüzünüzün açıklamasını oluşturur.
  2. Düzen: Kullanıcı arayüzünün yerleştirileceği yer. Bu aşama iki adımdan oluşur: ölçüm ve yerleşim. Düzenleme öğeleri, kendilerini ve tüm alt öğeleri, düzen ağacındaki her düğüm için 2D koordinatlarda ölçer ve yerleştirir.
  3. Çizim: Oluşturma şekli. Kullanıcı arayüzü öğeleri, genellikle bir cihaz ekranı olan bir tuvale çizilir.
Oluştur'un verileri kullanıcı arayüzüne dönüştürdüğü üç aşamayı gösteren bir resim (sırasıyla veri, kompozisyon, düzen, çizim, kullanıcı arayüzü).
Şekil 1. Oluşturma'nın verileri kullanıcı arayüzüne dönüştürdüğü üç aşama.

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

Kavramsal olarak bu aşamaların her biri her kare için gerçekleşir. Ancak Compose, performansı optimize etmek için bu aşamaların tümünde aynı girişlerden aynı sonuçları hesaplayacak çalışmaları tekrarlamaktan kaçınır. Compose, önceki bir sonucu yeniden kullanabiliyorsa bir derlenebilir işlevin çalıştırılmasını atlar ve Compose kullanıcı arayüzü, gerekmediği sürece ağacın tamamını yeniden düzenlemez veya yeniden çizmez. Oluşturma işlemi, yalnızca kullanıcı arayüzünü güncellemek için gereken minimum miktarda çalışmayı gerçekleştirir. Bu optimizasyon, Compose farklı aşamalardaki durum okumalarını izlediği için mümkündür.

Aşamaları anlama

Bu bölümde, üç derleme aşamasının derlenebilirler için nasıl yürütüldüğü daha ayrıntılı olarak açıklanmaktadır.

Beste

Oluşturma aşamasında Compose çalışma zamanı, birleştirilebilir 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 gereken tüm bilgileri içeren düzen düğümlerinden oluşur:

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

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

Beş bileşen ve alt düğümlerin üst düğümlerinden ayrıldığı, ortaya çıkan kullanıcı arayüzü ağacını içeren bir kod snippet'i.
Şekil 3. Kullanıcı arayüzü ağacının, ilgili kodu içeren alt bölümü.

Bu örneklerde, koddaki her bir birleştirilebilir işlev, kullanıcı arayüzü ağacında tek bir düzen düğümüyle eşlenir. Daha karmaşık örneklerde, bileşenler mantık ve kontrol akışı içerebilir ve farklı durumlara göre farklı bir ağaç oluşturabilir.

Düzen

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

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

Düzenleme aşamasında, ağaç aşağıdaki üç adımlı algoritma kullanılarak taranır:

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

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

  • Atanmış bir genişlik ve yükseklik
  • Çizileceği x, y koordinatı

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

Beş bileşen ve alt düğümlerin üst düğümlerinden ayrıldığı, ortaya çıkan kullanıcı arayüzü ağacını içeren bir kod snippet'i

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

  1. Row, alt öğeleri Image ve Column'yi ölçer.
  2. Image ölçülür. Alt öğesi olmadığından kendi boyutuna karar verir ve boyutu Row öğesine bildirir.
  3. Ardından Column ölçülür. Önce kendi alt öğelerini (iki Text bileşeni) ölçer.
  4. İlk Text ölçülür. Alt öğesi olmadığından kendi boyutuna karar verir ve boyutunu Column öğesine bildirir.
    1. İkinci Text ölçülür. Alt öğesi olmadığı için kendi boyutuna karar verir ve bunu Column öğesine bildirir.
  5. Column, kendi boyutuna karar vermek için alt öğe ölçümlerini kullanır. Maksimum alt öğe genişliğini ve alt öğelerinin yüksekliğinin toplamını kullanır.
  6. Column, alt öğelerini dikey olarak birbirinin altına yerleştirerek kendileriyle ilişkili olarak yerleştirir.
  7. Row, kendi boyutuna karar vermek için alt öğe ölçümlerini kullanır. Maksimum alt öğe yüksekliğini ve alt öğelerinin genişliklerinin toplamını kullanır. Ardından, alt öğelerini yerleştirir.

Her bir 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ında 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 gezinmek için harcanan süre doğrusal olarak artar. Buna karşılık, her düğüm birden çok kez ziyaret edildiyse tarama süresi katlanarak artar.

Çizim

Çizim aşamasında ağaç tekrar yukarıdan aşağıya doğru taranır ve her düğüm sırayla ekranda çizilir.

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

Önceki örnekte gösterilen ağaç içeriği aşağıdaki şekilde çizilir:

  1. Row, sahip olabileceği tüm içerikleri (ör. arka plan rengi) çizer.
  2. Image kendi kendini çizer.
  3. Column kendi kendini çizer.
  4. İlk ve ikinci Text sırasıyla kendilerini çizer.

Şekil 6. Bir kullanıcı arayüzü ağacı ve çizilmiş temsili.

Eyalet okumaları

Yukarıda listelenen aşamalardan birinde bir anlık görüntü durumunun değerini okuduğunuzda Oluştur, değer okunduğunda ne yaptığını otomatik olarak izler. Bu izleme, durum değeri değiştiğinde Compose'un okuyucuyu yeniden yürütmesine olanak tanır ve Compose'da durum gözlemlenebilirliğinin temelini oluşturur.

Durum genellikle mutableStateOf() kullanılarak oluşturulur ve ardından iki yoldan biriyle erişilir: doğrudan value mülküne erişerek veya alternatif olarak Kotlin mülk temsilcisi kullanılarak. Bunlar hakkında daha fazla bilgiyi Kompozitlerde durum başlıklı makalede bulabilirsiniz. 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 temsilcisinin altında, "alıcı" ve "ayarlayıcı" işlevleri, durumun value değerine erişmek ve bu değeri güncellemek için kullanılır. Bu alıcı ve ayarlayıcı işlevleri, yalnızca özellik oluşturulduğunda değil, değer olarak referans verdiğiniz zaman çağrılır. Bu nedenle, yukarıdaki iki yöntem eşdeğerdir.

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

Aşamalı durum okumaları

Yukarıda da belirtildiği gibi, Oluştur'da üç ana aşama vardır ve Oluştur, her bir aşamada hangi durumun okunduğunu izler. Bu sayede Compose, kullanıcı arayüzünüzün etkilenen her öğesi için işlem yapması gereken belirli aşamaları yalnızca bilgilendirir.

Her bir aşamaya göz atıp içinde bir durum değeri okunduğunda ne olacağını açıklayalım.

1. Aşama: Kompozisyon

@Composable işlevindeki veya lambda bloğundaki durum okumaları, bileşimi ve muhtemelen sonraki aşamaları etkiler. Durum değeri değiştiğinde yeniden derleyici, bu durum değerini okuyan tüm birleştirilebilir işlevlerin yeniden çalıştırılmasını planlar. Girişler değişmediyse çalışma zamanının, birleştirilebilir işlevlerin bir kısmını veya tamamını atlamaya karar verebileceğini unutmayın. Daha fazla bilgi için Girişler değişmediyse atlama başlıklı makaleyi inceleyin.

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

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üzenleme aşaması iki adımdan oluşur: ölçüm ve yerleşim. Ölçüm adımı, Layout bileşimine iletilen ölçüm lambda'sı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 { … } işlevinin lambda bloğunu vb. çalıştırır.

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

Daha açık belirtmek gerekirse, ölçüm adımı ve yerleşim adımı ayrı yeniden başlatma kapsamlarına sahiptir. Bu, yerleşim adımında durum okumalarının, ölçüm adımını daha önce yeniden çağırmadığı anlamına gelir. Ancak bu iki adım genellikle iç içe geçtiğinden, 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. Sık karşılaşılan örnekler arasında Canvas(), Modifier.drawBehind ve Modifier.drawWithContent bulunur. 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ını optimize etme

Oluşturma, yerelleştirilmiş durum okuma takibi gerçekleştirdiğinden, her durumu uygun bir aşamada okuyarak yapılan çalışma miktarını en aza indirebiliriz.

Bir örnekle açıklayalım. Burada, nihai düzen konumunu kaydıran offset değiştiriciyi kullanan bir Image() var. Bu, kullanıcı ekranı kaydırırken paralaks efekti oluşturur.

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 performans sağlamaz. Yazıldığı şekilde kod, firstVisibleItemScrollOffset durumunun değerini okur ve Modifier.offset(offset: Dp) işlevine iletir. Kullanıcı ekranı kaydırdığında firstVisibleItemScrollOffset değeri değişir. Oluşturma işlevi, okuma kodunu yeniden başlatabilmek (yeniden çağırabilmek) için tüm durum okumalarını izler. Bu kod, örneğimizde Box içeriğidir.

Bu, kompozisyon aşamasında okunan durum örneğidir. Bu durum her zaman kötü bir şey değildir. Hatta veri değişikliklerinin yeni kullanıcı arayüzü oluşturmasına olanak tanıyan yeniden oluşturmanın temelini oluşturur.

Ancak bu örnekte, her kaydırma etkinliği, derlenebilir içeriğin tamamının yeniden değerlendirilmesine, ardından ölçülmesine, düzenlenmesine ve son olarak çizilmesine neden olacağından bu yöntem optimal değildir. Gösterdiğimiz şey değişmemiş olsa da yalnızca nerede gösterildiği değiştiği için her kaydırma işleminde Oluştur aşamasını tetikliyoruz. Durum okumamızı yalnızca düzen aşamasını yeniden tetikleyecek şekilde optimize edebiliriz.

Ofset değiştiricinin başka bir sürümü de mevcuttur: Modifier.offset(offset: Density.() -> IntOffset).

Bu sürüm, bir lambda parametresi alır. Sonuçta ortaya çıkan ofset, lambda bloğu tarafından döndürülür. Bu özelliği kullanmak için kodumuzu 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 performanslı? Değiştiriciye sağladığımız lambda bloğu, düzenleme aşamasında (özellikle de düzen aşamasının yerleşim adımında) çağrılır. Bu, firstVisibleItemScrollOffset durumumuzun artık derleme sırasında okunmadığı anlamına gelir. Compose, durumun ne zaman okunduğunu izler. Bu nedenle, firstVisibleItemScrollOffset değeri değişirse Compose'un yalnızca düzen ve çizim aşamalarını yeniden başlatması gerekir.

Bu örnekte, ortaya çıkan kodu optimize edebilmek için farklı ofset değiştiricilerden yararlanılmaktadır ancak genel fikir doğrudur: Durum okumalarını mümkün olan en düşük aşamaya yerleştirmeye çalışın. Böylece, Compose'un minimum miktarda çalışma yapmasını sağlayabilirsiniz.

Elbette, genellikle kompozisyon aşamasında durumları okumak kesinlikle gereklidir. Bununla birlikte, durum değişikliklerini filtreleyerek yeniden derleme sayısını en aza indirebileceğimiz durumlar da vardır. Bu konu hakkında daha fazla bilgi için derivedStateOf: bir veya daha fazla durum nesnesini başka bir duruma dönüştürme başlıklı makaleyi inceleyin.

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ırada çağrıldığını ve aynı karedeyken geriye gitmenin mümkün olmadığını belirtmiştik. Ancak bu, uygulamaların farklı karelerde kompozisyon 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, üstte resim ve altında metin bulunan dikey bir sütun (kötü bir şekilde) uygulanmıştır. Resmin çözünür boyutunu öğrenmek için Modifier.onSizeChanged()'ü, metni aşağı kaydırmak için ise Modifier.padding()'ü kullanıyoruz. Px'ten Dp'a yapılan doğal olmayan dönüşüm, kodda bir sorun olduğunu zaten gösteriyor.

Bu örnekteki sorun, tek bir kare içinde "nihai" düzene ulaşmamamızdır. Kod, birden fazla karenin gerçekleşmesine dayanır. Bu da gereksiz işlemler yapılmasına ve kullanıcının ekranda UI'nin zıplamasına neden olur.

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

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

Oluştur, ardından imageHeightPx değerinin değişikliğine göre planlanan ikinci kareyi başlatır. Durum, Box içerik bloğunda okunur ve derleme aşamasında çağrılır. Bu sefer metin, resim yüksekliğine uygun bir dolguyla sağlanır. Kod, düzen aşamasında imageHeightPx değerini tekrar ayarlar ancak değer aynı kaldığı için yeniden oluşturma planlanmaz.

Sonuç olarak, metinde istenen dolguyu elde ederiz ancak dolgu değerini farklı bir aşamaya geri iletmek için ek bir kare harcamak optimal değildir ve çakışan içerik içeren bir kare oluşturmaya neden olur.

Bu örnek abartılı görünse de bu genel kalıba dikkat edin:

  • Modifier.onSizeChanged(), onGloballyPositioned() veya başka bir düzen işlemi
  • Bazı durumları güncelleme
  • Bu durumu bir düzen değiştiricinin (padding(),height() veya benzeri) girişi olarak kullanın
  • Tekrarlanabilir.

Yukarıdaki örnek için doğru düzen temel öğelerini kullanmak gerekir. Yukarıdaki örnek basit bir Column() ile uygulanabilir ancak özel bir şey gerektiren daha karmaşık bir örneğiniz olabilir. Bu durumda özel bir düzen yazmanız gerekir. Daha fazla bilgi için Özel düzenler kılavuzuna bakın.

Buradaki genel ilke, birbirine göre ölçülmesi ve yerleştirilmesi gereken birden fazla kullanıcı arayüzü öğesi için tek bir doğruluk kaynağına sahip olmaktır. Doğru bir düzen ilkelini kullanmak veya özel bir düzen oluşturmak, minimum paylaşılan üst öğenin birden fazla öğe arasındaki ilişkiyi koordine edebilecek doğruluk kaynağı olarak hizmet ettiği anlamına gelir. Dinamik bir durum eklemek bu ilkeyi ihlal eder.