En iyi uygulamaları izleme

Yaygın Oluşturma hatalarıyla karşılaşabilirsiniz. Bu hatalar, yeterince iyi çalıştığı görünen ancak kullanıcı arayüzü performansınızı olumsuz etkileyebilecek kodlar oluşturmanıza neden olabilir. Compose'da uygulamanızı optimize etmek için en iyi uygulamaları izleyin.

Pahalı hesaplamaları en aza indirmek için remember kullanma

Birleştirilebilir işlevler, animasyonun her karesi için olduğu kadar çok sık çalışabilir. Bu nedenle, composable'ınızın gövdesinde mümkün olduğunca az hesaplama yapmalısınız.

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

Örneğin, aşağıda adların sıralanmış bir listesini görüntüleyen ancak sıralamayı çok maliyetli bir şekilde yapan bir kod verilmiştir:

@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 sona yeniden sıralanır. Kullanıcı listede kaydırma yaptığında, yeni bir satır her göründüğünde Composable yeniden oluşturulur.

Bu sorunu çözmek için listeyi LazyColumn dışında sıralayın ve sıralanmış listeyi remember ile birlikte saklayı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 oluşturulduğunda bir kez sıralanıyor. Kişiler veya karşılaştırıcı değişirse sıralanmış liste yeniden oluşturulur. Aksi takdirde, composable, sıralanmış ve önbelleğe alınmış listeyi kullanmaya devam edebilir.

Lazy layout tuşlarını kullanma

Lazy layouts, öğeleri yalnızca gerektiğinde yeniden oluşturarak veya yeniden düzenleyerek verimli bir şekilde yeniden kullanır. Ancak, lazy layout'ların yeniden düzenleme için optimize edilmesine yardımcı olabilirsiniz.

Bir kullanıcı işleminin, listedeki bir öğenin taşınmasına neden olduğunu varsayalım. Örneğin, en son değiştirilen notun en üstte olduğu, değiştirilme zamanına göre sıralanmış bir not listesi 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. En alttaki notun değiştirildiğini varsayalım. Bu not artık en son değiştirilen not olduğu için listenin en üstüne taşınır ve diğer tüm notlar bir sıra aşağı iner.

Yazma, yardımınız olmadan değişmeyen öğelerin listede yalnızca taşındığını fark etmez. Bunun yerine, Oluşturma özelliği eski "öğe 2"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 Oluşturma, listedeki her öğeyi yeniden oluşturur.

Buradaki çözüm, öğe anahtarları sağlamaktır. Her öğe için sabit bir anahtar sağlamak, Compose'un gereksiz yeniden oluşturmalardan kaçınmasını sağlar. Bu durumda Compose, 3. sıradaki öğenin 2. sıradaki öğeyle aynı olduğunu belirleyebilir. Bu öğeyle ilgili verilerin hiçbiri değişmediğinden 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)
        }
    }
}

Yeniden oluşturma işlemlerini sınırlamak için derivedStateOf kullanma

Kompozisyonlarınızda durumu kullanmanın bir riski, durumun hızlı bir şekilde değişmesi durumunda kullanıcı arayüzünüzün gerekenden daha fazla yeniden oluşturulabilmesidir. Örneğin, kaydırılabilir bir liste gösterdiğinizi varsayalım. Listedeki ilk görünür öğenin hangisi olduğunu görmek için listenin durumunu incelersiniz:

val listState = rememberLazyListState()

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

val showButton = listState.firstVisibleItemIndex > 0

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

Buradaki sorun, kullanıcı listeyi kaydırdığında parmağını sürüklerken listState değerinin sürekli değişmesidir. Bu da listenin sürekli olarak yeniden oluşturulduğu anlamına gelir. Ancak, listenin bu kadar sık yeniden oluşturulması gerekmez. Yeni bir öğe altta görünene kadar yeniden oluşturmanız gerekmez. Bu nedenle, çok fazla ek hesaplama yapılır ve kullanıcı arayüzünüzün performansı düşer.

Çözüm, türetilmiş durumu kullanmaktır. Türetilmiş durum, Compose'a hangi durum değişikliklerinin yeniden oluşturmayı tetiklemesi gerektiğini söylemenize olanak tanır. Bu durumda, ilk görünür öğenin ne zaman değiştiğini önemsemenizi 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()
}

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

Performans sorunu tespit edildiğinde durum okumalarını ertelemek yardımcı 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ında yukarı taşınmış bir durum varsa ve durumu bir alt composable'da okuyorsanız durum okumasını bir lambda işlevine sarmalayabilirsiniz. Bunu yaptığınızda okuma yalnızca gerçekten gerektiğinde gerçekleşir. Referans için Jetsnack örnek uygulamasındaki uygulamaya bakın. Jetsnack, ayrıntı ekranında daraltılabilir araç çubuğu benzeri bir efekt uygular. Bu tekniğin neden işe yaradığını anlamak için Jetpack Compose: Debugging Recomposition başlıklı blog yayınını inceleyin.

Bu efekti elde etmek için Title composable'ın, Modifier kullanarak kendini kaydırması için kaydırma uzaklığına ihtiyacı vardır. Optimizasyon yapılmadan önceki Jetsnack kodunun basitleştirilmiş bir sürümü aşağıda verilmiştir:

@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 composable'dır. Box işlevinin satır içi bir işlev olduğunu ve bu nedenle yeniden oluşturma kapsamı olmadığını unutmayın. 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 öğe 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 durumda Title, yükseltilmiş duruma referans vermeye devam edebilir ancak değer yalnızca gerçekten ihtiyaç duyulduğu Title içinde okunur. Sonuç olarak, kaydırma değeri değiştiğinde en yakın yeniden oluşturma kapsamı artık Title composable'dır. Compose'un artık Box'ın tamamını yeniden oluşturması gerekmez.

Bu iyi bir gelişme olsa da daha iyisini yapabilirsiniz. Bir composable'ı yeniden düzenlemek veya yeniden çizmek için yeniden oluşturmaya neden oluyorsanız şüphelenmelisiniz. Bu durumda, yaptığınız tek şey Title composable'ın ofsetini değiştirmektir. 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()) }
    ) {
        // ...
    }
}

Daha önce, ofseti parametre olarak alan Modifier.offset(x: Dp, y: Dp) kodu kullanılıyordu. Değiştiricinin lambda sürümüne geçerek işlevin, düzen aşamasındaki kaydırma durumunu okumasını sağlayabilirsiniz. Sonuç olarak, kaydırma durumu değiştiğinde Compose, kompozisyon aşamasını tamamen atlayıp doğrudan düzen aşamasına geçebilir. Sık sık değişen durum değişkenlerini değiştiricilere aktarırken mümkün olduğunda değiştiricilerin lambda sürümlerini kullanmanız gerekir.

Bu yaklaşımla ilgili başka bir örnek aşağıda verilmiştir. 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şir. Daha sonra composable, bu durumu arka plan değiştiricisinde okur. Sonuç olarak, renk her karede değiştiğinden kutunun her karede yeniden oluşturulması gerekir.

Bunu iyileştirmek için lambda tabanlı bir değiştirici kullanın. Bu örnekte drawBehind değiştiricisi kullanılabilir. Bu, renk durumunun yalnızca çizim aşamasında okunduğu anlamına gelir. Sonuç olarak, renk değiştiğinde Compose, kompozisyon ve düzen aşamalarını tamamen atlayıp doğrudan çizim aşamasına geçebilir.

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

Geriye dönük yazma işlemlerinden kaçının

Oluşturma özelliğinin temel varsayımı, okunmuş bir metni asla tekrar yazmayacağınızdır. Bu işlem geriye doğru yazma olarak adlandırılır ve her karede sürekli olarak yeniden oluşturmaya neden olabilir.

Aşağıdaki composable'da bu tür bir hatanın örneği gösterilmektedir.

@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, önceki satırda okunduktan sonra composable'ın sonundaki sayımı günceller. Bu kodu çalıştırırsanız düğmeyi tıkladıktan sonra sayacın sonsuz bir döngüde hızla arttığını görürsünüz. Bunun nedeni, Compose'un bu composable'ı yeniden oluşturması, güncel olmayan bir durum okuması görmesi ve bu nedenle başka bir yeniden oluşturma planlamasıdır.

Composition'da 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ğindeki gibi bir lambda'da durum belirtmek için yazın.

Ek kaynaklar