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
Bundleiçine kaydedilebilir olmalıdır. Çünkü geç yüklenen düzenler dahili olarakrememberSaveablekullanır.Int,LongveyaStringgibi temel türleri tercih edin ve anahtar olarak hiçbir zaman liste dizinlerini veyahashCode()kullanmayın. - Heterojen listeler için her zaman
contentTypebelirtin: 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çincontentType = { it.type }öğesiniitemsveyaitemsIndexedöğesine iletin. indexOfyerineitemsIndexedkullanın:itemsbloğundalist.indexOf(item)çağrısı yapmaktan kaçının. Bu, düzeniO(N²)taramasına dönüştürür. Bunun yerineitemsIndexedkullanın.- Paylaşılan durumu ve
CompositionLocalokumalarını yukarı taşıyın: PaylaşılanFlowörneklerini toplayın veCompositionLocaldeğerlerini (ör.LocalContext.currentveyaLocalDensity.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
RowveyaColumnkullanın: Küçük statik veya sınırlı koleksiyonlar (ör. 1 ila 10 öğe) içinforEachiçeren standart birRowveyaColumn, 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çinremember { 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 okunuyorsaderivedStateOfkullanın.derivedStateOfiçinde okunan anlık görüntü durumu değerlerini (ör.listState.firstVisibleItemIndex)rememberanahtarı olarak iletmeyin. Aksi takdirde, her durum değişikliğindeDerivedStateyeniden 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 koleksiyonuderivedStateOfiçine sarmak yerine doğrudan.sizeokuyun. Eşzamanlı durum türetmeleri içinsnapshotFlowileproduceStatekullanmaktan 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
- Uygulama performansı kılavuzu: Android'de performansı artırmak için en iyi uygulamaları, kitaplıkları ve araçları keşfedin.
- Performansı İnceleme: Uygulama performansını inceleyin.
- Karşılaştırma: Uygulama performansını karşılaştırın.
- Uygulama başlatma: Uygulama başlatmayı optimize edin.
- Temel profiller: Temel profilleri anlama.
Sizin için önerilenler
- Not: JavaScript kapalıyken bağlantı metni gösterilir.
- State ve Jetpack Compose
- Grafik Değiştiriciler
- Compose'da düşünme