En iyi uygulamaları izleme

Oluşturma işleminde sık karşılaşılan hatalarla karşılaşabilirsiniz. Bu hatalar size yeterince iyi çalışan bir kod verebilir ancak kullanıcı arayüzü performansınızı olumsuz etkileyebilir. Uygulamanızı Compose'da optimize etmek için en iyi uygulamaları takip edin.

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

Oluşturulabilir işlevler, bir animasyonun her karesi kadar çok sık çalışabilir. Bu nedenle, composable'ınızda mümkün olduğunca az hesaplama yapmanız gerekir.

Hesaplamaların sonuçlarını remember ile depolamak da önemli bir tekniktir. Bu şekilde, hesaplama bir kez çalışır ve sonuçları gerektiğinde alabilirsiniz.

Örneğin, aşağıda adların sıralı listesini görüntüleyen, ancak sıralamayı çok pahalı bir şekilde yapan bir kodu görebilirsiniz:

@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 kişi listesinin tamamı baştan sona sıralanır. Kullanıcı listeyi kaydırırsa yeni bir satır göründüğünde Composable yeniden düzenlenir.

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

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

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

Liste artık ContactList ilk kez oluşturulurken bir kez sıralanır. Kişiler veya karşılaştırıcılar değişirse sıralanmış liste yeniden oluşturulur. Aksi takdirde, composable, önbelleğe alınmış sıralı listeyi kullanmaya devam edebilir.

Geç düzen tuşlarını kullanma

Geç düzenler, öğeleri yalnızca gerektiğinde yeniden oluşturarak veya yeniden oluşturarak verimli bir şekilde yeniden kullanır. Ancak geç düzenleri yeniden oluşturma için optimize etmeye yardımcı olabilirsiniz.

Bir kullanıcı işleminin, bir öğenin listede taşınmasına neden olduğunu varsayalım. Örneğin, değişiklik zamanına göre sıralanmış bir not listesi gösterdiğinizi ve en son değiştirilen not en üstte yer aldığını 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, en son değiştirilen nottur ve listenin başına gider ve diğer tüm notlar bir sıra aşağıya taşınır.

Compose sizin yardımınız olmadan listedeki değişmemiş öğelerin yalnızca taşındığını fark etmez. Bunun yerine Compose eski "2. öğe"nin silindiğini ve 3., 4. öğe ile tamamen aşağısı için yeni bir öğe oluşturulduğunu düşünür. Sonuç olarak Best, listedeki her öğeyi değiştirmiş olsa bile bu öğeleri yeniden oluşturur.

Bu sorunu çözmek için öğe anahtarları sağlayabilirsiniz. Her öğe için sabit bir anahtar sağlanması, Compose'un gereksiz yeniden derlemelerden kaçınmasını sağlar. Bu durumda Compose 3. noktada bulunan öğenin daha önce 2. noktadaki öğeyle aynı olduğunu belirleyebilir. Söz konusu öğenin verileri değişmediğinden, Compose'un öğeyi yeniden oluşturmasına gerek yoktur.

@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ırlandırmak için derivedStateOf kullanın

Bestelerinizde durum kullanımının risklerinden biri, durum hızlı bir şekilde değişirse kullanıcı arayüzünüzün gereksinim duyduğunuzdan daha fazla yeniden oluşturulabilmesidir. Örneğin, kaydırılabilir bir liste görüntülediğinizi varsayalım. Hangi öğenin listedeki ilk görünür öğe olduğunu görmek için listenin durumunu incelersiniz:

val listState = rememberLazyListState()

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

val showButton = listState.firstVisibleItemIndex > 0

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

Burada sorun, kullanıcı listeyi kaydırdığında listState parmağını sürükledikçe sürekli değişmektedir. Bu da listenin sürekli olarak yeniden derlendiği anlamına gelir. Bununla birlikte, dosyayı o kadar sık yeniden oluşturmanız gerekmez. Altta yeni bir öğe görünene kadar içeriği yeniden düzenlemeniz gerekmez. Dolayısıyla bu, çok fazla hesaplama yapılması anlamına gelir ve kullanıcı arayüzünüz kötü performans gösterir.

Çözüm, türetilmiş durum özelliğini kullanmaktır. Türetilmiş durum, Compose'a hangi durum değişikliklerinin yeniden düzenlemeyi gerçekten tetikleyeceğini bildirebilmenizi sağlar. Bu durumda, ilk görünür öğenin değişmesine önem verdiğinizi belirtin. Bu durum değeri değiştiğinde kullanıcı arayüzünün yeniden oluşturulması gerekir ancak kullanıcı henüz yeni öğeyi en üste getirmek için kaydırmadıysa öğenin yeniden oluşturması gerekmez.

val listState = rememberLazyListState()

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

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

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

Okumaları mümkün olduğunca ertele

Bir performans sorunu tespit edildiğinde, durum okumalarını ertelemek yardımcı olabilir. Durum okumalarının ertelenmesi, Compose'un yeniden oluşturma sırasında mümkün olan en az sayıda kodu yeniden çalıştırmasını sağlar. Örneğin, kullanıcı arayüzünüzün durumu composable ağacında yukarı kaldırılmışsa ve durumu bir alt composable'da okursanız bir lambda işlevinde okunan durumu sarmalayabilirsiniz. Böylece, okuma yalnızca gerçekten gerekli olduğunda gerçekleşir. Referans olarak Jetsnack örnek uygulamasındaki uygulamaya bakın. Jetsnack, ayrıntı ekranında daraltma araç çubuğu benzeri bir efekt uygular. Bu tekniğin neden işe yaradığını anlamak için Jetpack Compose: Debugging Recomposition blog yayınına bakın.

Bu efekti elde etmek için Title composable, Modifier kullanarak kendisini dengelemek amacıyla kaydırma ofsetine ihtiyaç duyar. Aşağıda, optimizasyon yapılmadan önceki Jeetsnack kodunun basitleştirilmiş bir sürümü 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, Oluştur, 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 bir satır içi işlev olduğunu ve yeniden derleme 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 lambda oldu. Bu, Title öğesinin kaldırılıp kaldırılma durumuna hâlâ başvurabileceği anlamına gelir. Ancak, değer yalnızca gerçekten gerekli olan 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 olur. Artık Composer'ın Box öğesinin tamamını yeniden oluşturmasına gerek yoktur.

Bu iyi bir gelişme olsa da siz daha iyisini yapabilirsiniz. Yalnızca bir Composable'ı yeniden düzenlemek ya da yeniden çizmek için yeniden kompozisyona neden oluyorsanız şüpheyle yaklaşmalısınız. Bu durumda, tek yapmanız gereken Title composable'ın ofsetini değiştirmektir. Bu değişiklik, 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 kodda, ofseti parametre olarak alan Modifier.offset(x: Dp, y: Dp) kullanılı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. Sonuç olarak, kaydırma durumu değiştiğinde Compose oluşturma aşamasını tamamen atlayarak doğrudan düzen aşamasına geçebilir. Sık sık Durum değişkenlerini değiştiricilere geçirirken, mümkün olduğunda değiştiricilerin lambda sürümlerini kullanmanız gerekir.

Aşağıda, bu yaklaşımın başka bir örneği 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ızlı bir şekilde değişiyor. Dolayısıyla bu durum çok sık değişmektedir. Ardından composable, arka plan değiştiricisinde bu durumu 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). Bu, renk durumunun yalnızca çizim aşamasında okunacağı anlamına gelir. Sonuç olarak, Oluştur uygulaması beste ve düzen aşamalarını tamamen atlayabilir. Renk değiştiğinde, Oluştur 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 yazmalardan kaçınma

Compose'da zaten okunmuş olan bir e-postaya hiçbir zaman yazmayacağınıza dair temel bir varsayım vardır. Bunu yaptığınızda, geriye doğru yazma adı verilir ve sürekli olarak her karede yeniden birleştirme yapılmasına neden olabilir.

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

@Composable
fun BadComposable() {
    var count by remember { mutableStateOf(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, composable'ı bir önceki satırda okuduktan sonra sonundaki sayıyı günceller. Bu kodu çalıştırırsanız düğmeyi tıkladığınızda, yeniden oluşturma işlemi yapıldığında sayacın hızla sonsuz bir döngü içinde yükseldiğini görürsünüz. Bunun nedeni, Oluştur'un bu Composable'ı yeniden derlemesi, okunmuş bir durumu okuması ve dolayısıyla yeni bir yeniden oluşturma planlaması yapmasıdır.

Hiçbir zaman Beste'de durum bölümüne yazmazsanız geriye doğru yazmalardan tamamen kaçınabilirsiniz. Mümkünse önceki onClick örneğindeki gibi bir etkinliğe yanıt olarak ve bir lambda içinde durumu daima yazın.

Ek Kaynaklar