En iyi uygulamaları izleme

Compose'da sık karşılaşılan sorunlarla karşılaşabilirsiniz. Bu hatalar, yeterince iyi çalışan ancak kullanıcı arayüzü performansınıza zarar verebilecek kodlar almanıza neden olabilir. Compose'da uygulamanızı optimize etmek için en iyi uygulamaları izleyin.

Maliyetli hesaplamaları en aza indirmek için remember kullanın

Bir animasyonun her karesi için çalıştırılabilecek kadar sık çalıştırılabilen composable işlevler. Bu nedenle, composable'ınızın gövdesinde mümkün olduğunca az hesaplama yapmanız gerekir.

Önemli bir teknik, hesaplama sonuçlarını remember ile depolamaktır. Bu sayede hesaplama bir kez çalıştırılır ve sonuçları gerektiğinde alabilirsiniz.

Örneğin, aşağıdaki kodda sıralı bir ad listesi gösteriliyor ancak sıralama işlemi çok maliyetli bir şekilde yapılıyor:

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    LazyColumn(modifier) {
        // DON’T DO THIS
        items(contacts.sortedWith(comparator)) { contact ->
            // ...
        }
    }
}

ContactsList her yeniden oluşturulduğunda, liste değişmemiş olsa bile tüm kişi listesi baştan sıralanır. Kullanıcı listeyi kaydırırsa yeni bir satır göründüğünde Composable yeniden oluşturulur.

Bu sorunu çözmek için listeyi LazyColumn dışında sıralayın ve sıralanan listeyi remember ile depolayın:

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    val sortedContacts = remember(contacts, comparator) {
        contacts.sortedWith(comparator)
    }

    LazyColumn(modifier) {
        items(sortedContacts) {
            // ...
        }
    }
}

Artık liste, ContactList ilk kez oluşturulduğunda bir kez sıralanır. Kişiler veya karşılaştırıcı değişirse sıralanan liste yeniden oluşturulur. Aksi takdirde, birleştirilebilir öğe önbelleğe alınmış sıralı listeyi kullanmaya devam edebilir.

remember hesaplama bloğunda referans verilen tüm girişleri her zaman anahtar olarak iletin. Böylece, bu girişler değiştiğinde önbelleğe alınan değer güncellenir. remember, anahtarlarını ve önbelleğe alınmış sonucunu yuva tablosunda sakladığından ve her yeniden oluşturma geçişinde Object.equals() kullanarak anahtarları karşılaştırdığından remember işlevini yalnızca yüksek maliyetli hesaplamaları veya ağır nesne ayırmalarını (tarih biçimlendiriciler, gölgelendiriciler veya liste sıralama gibi) önbelleğe alırken ya da nesne kimliğinin korunması gerektiğinde kullanın.

Düşük maliyetli bir hesaplama için remember anahtarı olarak büyük bir koleksiyon veya pahalı bir equals() uygulamasına sahip bir nesne iletirken dikkatli olun. Anahtarı karşılaştırmak, hesaplamayı doğrudan çalıştırmaktan daha maliyetli olabilir:

@Composable
fun ContactHeader(contacts: List<Contact>) {
    // Avoid: Comparing the `contacts` list with .equals() on recomposition
    // is more expensive than reading contacts.size or contacts.firstOrNull() directly.
    val firstContactName = remember(contacts) {
        contacts.firstOrNull()?.name.orEmpty()
    }
    Text(firstContactName)
}

Yavaş düzen anahtarlarını ve içerik türlerini kullanma

Yavaş düzenler, öğeleri verimli bir şekilde yeniden kullanır ve yalnızca gerektiğinde yeniden oluşturur veya yeniden oluşturur. Ancak, geç yüklenen düzenleri yeniden oluşturma ve alan yeniden kullanımı için optimize edebilirsiniz.

Bir kullanıcı işleminin, listedeki bir öğenin taşınmasına neden olduğunu varsayalım. Örneğin, notları değiştirilme zamanına göre sıraladığınızı ve en son değiştirilen notu en üstte gösterdiğinizi varsayalım.

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes
        ) { note ->
            NoteRow(note)
        }
    }
}

Ancak bu kodla ilgili bir sorun var. Alt notun değiştirildiğini varsayalım. Bu not artık en son değiştirilen not olduğundan listenin en üstüne taşınır ve diğer tüm notlar bir sıra aşağı kayar.

Yardımınız olmadan Compose, değişmeyen öğelerin listede yalnızca taşındığını fark etmez. Bunun yerine Compose, eski "2. öğe"nin silindiğini ve 3. öğe, 4. öğe ve diğer tüm öğeler için yeni bir öğe oluşturulduğunu düşünür. Sonuç olarak, yalnızca bir öğe değişmiş olsa bile Compose, listedeki her öğeyi yeniden oluşturur.

Bu sorunu çözmek için öğe anahtarları sağlamanız gerekir. Her öğe için sabit bir anahtar sağlamak, Compose'un gereksiz yeniden oluşturma işlemlerini önlemesini sağlar. Bu durumda Compose, şu anda 3. sırada olan öğenin daha önce 2. sırada olan öğeyle aynı olduğunu belirleyebilir. Bu öğenin verilerinden hiçbiri değişmediği için Compose'un öğeyi yeniden oluşturması gerekmez.

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes,
            key = { note ->
                // Return a stable, unique key for the note
                note.id
            }
        ) { note ->
            NoteRow(note)
        }
    }
}

Yavaş düzenlerle (LazyColumn, LazyRow, LazyGrid) çalışırken aşağıdaki yönergeleri de göz önünde bulundurun:

  • Pakete kaydedilebilir, benzersiz anahtarlar kullanın: Anahtarlar benzersiz, belirleyici ve Android Bundle içine kaydedilebilir olmalıdır. Çünkü geç yüklenen düzenler dahili olarak rememberSaveable kullanır. Int, Long veya String gibi temel türleri tercih edin ve anahtar olarak hiçbir zaman liste dizinlerini veya hashCode() kullanmayın.
  • Heterojen listeler için her zaman contentType belirtin: Kaydırma sırasında Compose'un yalnızca aynı görsel yapıyı paylaşan öğeler arasındaki oluşturma alanlarını yeniden kullanması için contentType = { it.type } öğesini items veya itemsIndexed öğesine iletin.
  • indexOf yerine itemsIndexed kullanın: items bloğunda list.indexOf(item) çağrısı yapmaktan kaçının. Bu, düzeni O(N²) taramasına dönüştürür. Bunun yerine itemsIndexed kullanın.
  • Paylaşılan durumu ve CompositionLocal okumalarını yukarı taşıyın: Paylaşılan Flow örneklerini toplayın ve CompositionLocal değerlerini (ör. LocalContext.current veya LocalDensity.current) bir kez lazy düzenin üzerindeki üst composable'da okuyun ve çözümlenen değerleri alt öğe composable'larına iletin.
  • Küçük, sınırlı listeler için Row veya Column kullanın: Küçük statik veya sınırlı koleksiyonlar (ör. 1 ila 10 öğe) için forEach içeren standart bir Row veya Column, geç sanallaştırmayı ve önceden getirme ek yükünü önler.

Yeniden oluşturma işlemlerini sınırlamak için computedStateOf veya derivedStateOf kullanın

Oluşturmalarınızda durum kullanmanın bir riski, durumun hızla değişmesi durumunda kullanıcı arayüzünüzün ihtiyacınız olandan daha fazla yeniden oluşturulabilmesidir. Örneğin, kaydırılabilir bir liste gösterdiğinizi varsayalım. Listenin durumunu inceleyerek listede görünen ilk öğenin hangisi olduğunu bulursunuz:

val listState = rememberLazyListState()

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

val showButton = listState.firstVisibleItemIndex > 0

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

Buradaki sorun, kullanıcı listeyi kaydırdığında, kullanıcı parmağını sürüklerken listState değerinin sürekli değişmesidir. Yani liste sürekli olarak yeniden oluşturuluyor. Ancak bu kadar sık yeniden oluşturmanız gerekmez. Yeni bir öğe en üstte görünene kadar yeniden oluşturmanız gerekmez. Bu da çok fazla ekstra hesaplama demektir ve kullanıcı arayüzünüzün performansını düşürür.

Çözüm, türetilmiş durum (veya Compose Runtime 1.13 ve sonraki sürümlerde computedStateOf) kullanmaktır. Türetilmiş durum, Compose'a hangi durum değişikliklerinin yeniden oluşturmayı tetiklemesi gerektiğini bildirmenizi sağlar. Bu durumda, ilk görünür öğenin ne zaman değiştiğini önemsediğinizi belirtin. Bu durum değeri değiştiğinde kullanıcı arayüzünün yeniden oluşturulması gerekir ancak kullanıcı yeni bir öğeyi en üste getirecek kadar kaydırma yapmadıysa yeniden oluşturulması gerekmez.

val listState = rememberLazyListState()

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

val showButton by remember {
    derivedStateOf {
        listState.firstVisibleItemIndex > 0
    }
}

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

Compose sürümünüze ve hesaplama maliyetine göre uygun durum türetme yaklaşımını seçin:

  • remember { computedStateOf { ... } } (Compose Runtime 1.13 ve sonraki sürümler): Sonucu önbelleğe almadan her okuma işleminde hesaplamasını yeniden değerlendirir ve yalnızca hesaplanan çıkış değiştiğinde oluşturmayı geçersiz kılar. Önbellek defter tutma ve geçersiz kılma geri yazma ek yükünü önlediği için temel koşul kontrolleri ve hızlı değişen durumlardan (kaydırma ofsetleri gibi) kaynaklanan basit türetmeler için remember { computedStateOf { listState.firstVisibleItemIndex > 0 } } tercih edin.
  • remember { derivedStateOf { ... } }: Birden fazla okuma işleminde hesaplama sonucunu önbelleğe alır. 1.13'ten önceki Compose sürümlerinde veya hesaplamanın kendisi işlem açısından maliyetliyse ve bir oluşturma aşamasında birden çok kez okunuyorsa derivedStateOf kullanın. derivedStateOf içinde okunan anlık görüntü durumu değerlerini (ör. listState.firstVisibleItemIndex) remember anahtarı olarak iletmeyin. Aksi takdirde, her durum değişikliğinde DerivedState yeniden oluşturulur ve atlama engellenir.
  • Basit Kotlin getter'ları (get()) ve doğrudan özellik okumaları: Geçersiz kılma azaltmanın gerekli olmadığı önemsiz boole ifadeleri için basit bir özel getter tercih edin ve koleksiyonu derivedStateOf içine sarmak yerine doğrudan .size okuyun. Eşzamanlı durum türetmeleri için snapshotFlow ile produceState kullanmaktan kaçının. Bu, gereksiz eş yordam dağıtım ek yüküne neden olur.

Okuma işlemlerini mümkün olduğunca erteleme

Bir performans sorunu tespit edildiğinde durum okuma işlemlerini ertelemek faydalı olabilir. Durum okumalarını ertelemek, Compose'un yeniden oluşturma sırasında mümkün olan en az kodu yeniden çalıştırmasını sağlar. Örneğin, kullanıcı arayüzünüzde composable ağacın üst kısımlarında yer alan bir durum varsa ve bu durumu alt composable öğede okuyorsanız durum okuma işlemini bir lambda fonksiyonuna sarmalayabilirsiniz. Böylece okuma işlemi yalnızca gerçekten gerektiğinde yapılır. Referans için Jetsnack örnek uygulamasındaki uygulamayı inceleyin. Jetsnack, ayrıntı ekranında daralan araç çubuğu benzeri bir efekt uygular. Bu tekniğin neden işe yaradığını anlamak için Jetpack Compose: Debugging Recomposition (Jetpack Compose: Yeniden Oluşturma İşlemlerinde Hata Ayıklama) başlıklı blog yayınını inceleyin.

Bu efekti elde etmek için Title composable'ının, Modifier kullanarak kendisini dengelemek amacıyla kaydırma ofsetine ihtiyacı vardır. Optimizasyon yapılmadan önce Jetsnack kodunun basitleştirilmiş bir sürümünü burada görebilirsiniz:

@Composable
fun SnackDetail() {
    // ...

    Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack, scroll.value)
        // ...
    } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scroll: Int) {
    // ...
    val offset = with(LocalDensity.current) { scroll.toDp() }

    Column(
        modifier = Modifier
            .offset(y = offset)
    ) {
        // ...
    }
}

Kaydırma durumu değiştiğinde Compose, en yakın üst yeniden oluşturma kapsamını geçersiz kılar. Bu durumda en yakın kapsam, SnackDetail bileşenidir. Box satır içi bir işlev olduğundan yeniden oluşturma kapsamı değildir. Bu nedenle Compose, SnackDetail ve SnackDetail içindeki tüm composable'ları yeniden oluşturur. Kodunuzu yalnızca gerçekten kullandığınız durumu okuyacak şekilde değiştirirseniz yeniden oluşturulması gereken öğelerin sayısını azaltabilirsiniz.

@Composable
fun SnackDetail() {
    // ...

    Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack) { scroll.value }
        // ...
    } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    val offset = with(LocalDensity.current) { scrollProvider().toDp() }
    Column(
        modifier = Modifier
            .offset(y = offset)
    ) {
        // ...
    }
}

Kaydırma parametresi artık bir lambda. Bu, Title öğesinin yukarı taşınan duruma hâlâ referans verebileceği ancak değerin yalnızca gerçekten ihtiyaç duyulan Title içinde okunacağı anlamına gelir. Sonuç olarak, kaydırma değeri değiştiğinde en yakın yeniden oluşturma kapsamı artık Title composable olur. Compose'un artık Box öğesinin tamamını yeniden oluşturması gerekmez.

Bu iyi bir gelişme ancak daha iyisini yapabilirsiniz. Yalnızca bir Composable'ı yeniden düzenlemek veya yeniden çizmek için yeniden oluşturmaya neden oluyorsanız şüphelenmeniz gerekir. Bu durumda, yalnızca Title composable'ının ofsetini değiştirirsiniz. Bu işlem, düzen aşamasında yapılabilir.

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    Column(
        modifier = Modifier
            .offset { IntOffset(x = 0, y = scrollProvider()) }
    ) {
        // ...
    }
}

Kod daha önce, ofseti parametre olarak alan Modifier.offset(x: Dp, y: Dp) yöntemini kullanıyordu. Değiştiricinin lambda sürümüne geçerek işlevin, düzen aşamasında kaydırma durumunu okumasını sağlayabilirsiniz. Bu sayede, kaydırma durumu değiştiğinde Compose, oluşturma aşamasını tamamen atlayıp doğrudan düzen aşamasına geçebilir. Sık değişen State değişkenlerini değiştiricilere aktarırken mümkün olduğunca değiştiricilerin lambda sürümlerini kullanmanız gerekir.

Bu yaklaşımın başka bir örneğini inceleyelim. Bu kod henüz optimize edilmedi:

// Here, assume animateColorBetween() is a function that swaps between
// two colors
val color by animateColorBetween(Color.Cyan, Color.Magenta)

Box(
    Modifier
        .fillMaxSize()
        .background(color)
)

Burada, kutunun arka plan rengi iki renk arasında hızla değişiyor. Bu nedenle bu durum çok sık değişiyor. Ardından composable, arka plan değiştiricide bu durumu okur. Bu nedenle, renk her karede değiştiği için kutunun her karede yeniden oluşturulması gerekir.

Bunu iyileştirmek için lambda tabanlı bir değiştirici (bu örnekte drawBehind) kullanın. Yani renk durumu yalnızca çizim aşamasında okunur. Sonuç olarak, Compose, oluşturma ve düzen aşamalarını tamamen atlayabilir. Renk değiştiğinde Compose doğrudan çizim aşamasına geçer.

val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(
    Modifier
        .fillMaxSize()
        .drawBehind {
            drawRect(color)
        }
)

Geriye doğru yazma işlemlerinden kaçınma

Compose'un temel varsayımı, daha önce okunmuş bir duruma hiçbir zaman yazmayacağınızdır. Bu işlem, geriye doğru yazma olarak adlandırılır ve her karede sonsuz bir şekilde yeniden oluşturmaya neden olabilir.

Aşağıdaki composable, bu tür bir hatanın örneğini göstermektedir.

@Composable
fun BadComposable() {
    var count by remember { mutableIntStateOf(0) }

    // Causes recomposition on click
    Button(onClick = { count++ }, Modifier.wrapContentSize()) {
        Text("Recompose")
    }

    Text("$count")
    count++ // Backwards write, writing to state after it has been read</b>
}

Bu kod, bir önceki satırda okuduktan sonra composable'ın sonundaki sayıyı günceller. Bu kodu çalıştırırsanız düğmeyi tıkladıktan sonra (bu işlem yeniden oluşturmaya neden olur) Compose bu Composable'ı yeniden oluştururken, güncel olmayan bir durum okuma işlemi görür ve başka bir yeniden oluşturma işlemi planlar. Bu nedenle, sayaç sonsuz bir döngüde hızla artar.

Oluşturma sırasında hiçbir zaman duruma yazmayarak geriye doğru yazma işlemlerini tamamen önleyebilirsiniz. Mümkünse her zaman bir etkinliğe yanıt olarak ve önceki onClick örneğinde olduğu gibi bir lambda içinde duruma yazın.

Geriye doğru yazma, yeniden oluşturma döngüleri ve aşama koordinasyonu hakkında ayrıntılı bir kılavuz için Compose'da geriye doğru yazma başlıklı makaleyi inceleyin.

Ek kaynaklar