Lazy listeler ve lazy ızgaralar

Birçok uygulamanın öğe koleksiyonlarını göstermesi gerekir. Bu dokümanda, Jetpack Compose'da bunu nasıl verimli bir şekilde yapabileceğiniz açıklanmaktadır.

Kullanım alanınızda kaydırma gerekmediğini biliyorsanız basit bir Column veya Row (yöne bağlı olarak) kullanabilir ve her öğenin içeriğini aşağıdaki gibi bir liste üzerinde yineleyerek yayınlayabilirsiniz:

@Composable
fun MessageList(messages: List<Message>) {
    Column {
        messages.forEach { message ->
            MessageRow(message)
        }
    }
}

verticalScroll() değiştiricisini kullanarak Column öğesini kaydırılabilir hale getirebiliriz.

Yavaş listeler

Çok sayıda öğe (veya uzunluğu bilinmeyen bir liste) görüntülemeniz gerekiyorsa Column gibi bir düzen kullanmak, görünür olup olmadıklarına bakılmaksızın tüm öğeler oluşturulup düzenleneceğinden performans sorunlarına neden olabilir.

Compose, yalnızca bileşenin görüntü alanında görünen öğeleri oluşturan ve düzenleyen bir dizi bileşen sunar. Bu bileşenler arasında LazyColumn ve LazyRow yer alır.

Adından da anlaşılacağı üzere LazyColumn ile LazyRow arasındaki fark, öğelerini düzenleme ve kaydırma yönleridir. LazyColumn dikey olarak kaydırılan bir liste, LazyRow ise yatay olarak kaydırılan bir liste oluşturur.

Lazy bileşenleri, Compose'daki çoğu düzenden farklıdır. Lazy bileşenleri, uygulamaların doğrudan composable'lar yayınlamasına olanak tanıyan bir @Composable içerik bloğu parametresi kabul etmek yerine LazyListScope.() bloğu sağlar. Bu LazyListScope bloğu, uygulamaların öğe içeriklerini açıklamasına olanak tanıyan bir DSL sunar. Ardından Lazy bileşeni, her öğenin içeriğini düzene ve kaydırma konumuna göre ekler.

LazyListScope DSL

LazyListScope'ın DSL'si, düzendeki öğeleri tanımlamak için çeşitli işlevler sunar. En temel düzeyde, item() tek bir öğe ekler ve items(Int) birden fazla öğe ekler:

LazyColumn {
    // Add a single item
    item {
        Text(text = "First item")
    }

    // Add 5 items
    items(5) { index ->
        Text(text = "Item: $index")
    }

    // Add another single item
    item {
        Text(text = "Last item")
    }
}

Ayrıca, List gibi öğe koleksiyonları eklemenize olanak tanıyan bir dizi uzantı işlevi de vardır. Bu uzantılar, yukarıdaki Column örneğimizi kolayca taşımamıza olanak tanır:

/**
 * import androidx.compose.foundation.lazy.items
 */
LazyColumn {
    items(messages) { message ->
        MessageRow(message)
    }
}

items() uzantı işlevinin, dizini sağlayan itemsIndexed() adlı bir varyantı da vardır. Daha fazla bilgi için lütfen LazyListScope referansına bakın.

Lazy grid'ler

LazyVerticalGrid ve LazyHorizontalGrid oluşturulabilir öğeleri, öğelerin ızgarada gösterilmesini destekler. Lazy dikey ızgara, öğelerini dikey olarak kaydırılabilir bir kapsayıcıda birden fazla sütuna yayılmış şekilde gösterir. Lazy yatay ızgaralar ise yatay eksende aynı davranışı gösterir.

Izgaralar, listelerle aynı güçlü API özelliklerine sahiptir ve içeriği tanımlamak için çok benzer bir DSL (LazyGridScope.()) kullanır.

Fotoğrafların gösterildiği bir telefon ekranının görüntüsü

LazyVerticalGrid içindeki columns parametresi ve LazyHorizontalGrid içindeki rows parametresi, hücrelerin sütun veya satır olarak nasıl oluşturulacağını kontrol eder. Aşağıdaki örnekte, her sütunun genişliğini en az 128.dp olarak ayarlamak için GridCells.Adaptive kullanılarak öğeler bir ızgarada gösterilmektedir:

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 128.dp)
) {
    items(photos) { photo ->
        PhotoItem(photo)
    }
}

LazyVerticalGrid, öğeler için bir genişlik belirtmenize olanak tanır. Ardından, ızgara mümkün olduğunca çok sütun sığdırır. Sütun sayısı hesaplandıktan sonra kalan genişlik, sütunlar arasında eşit olarak dağıtılır. Bu uyarlanabilir boyutlandırma yöntemi, özellikle farklı ekran boyutlarında öğe gruplarını görüntülemek için kullanışlıdır.

Kullanılacak sütunların tam sayısını biliyorsanız bunun yerine gerekli sütun sayısını içeren bir GridCells.Fixed örneği sağlayabilirsiniz.

Tasarımınızda yalnızca belirli öğelerin standart olmayan boyutlara sahip olması gerekiyorsa öğeler için özel sütun aralıkları sağlamak üzere ızgara desteğini kullanabilirsiniz. item ve items yöntemlerinin LazyGridScope DSLspan parametresiyle sütun aralığını belirtin. maxLineSpan, aralık kapsamının değerlerinden biridir ve sütun sayısı sabit olmadığından uyarlanabilir boyutlandırma kullanırken özellikle yararlıdır. Bu örnekte, tam satır aralığının nasıl sağlanacağı gösterilmektedir:

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 30.dp)
) {
    item(span = {
        // LazyGridItemSpanScope:
        // maxLineSpan
        GridItemSpan(maxLineSpan)
    }) {
        CategoryCard("Fruits")
    }
    // ...
}

Lazy staggered grid

LazyVerticalStaggeredGrid ve LazyHorizontalStaggeredGrid, öğelerin geç yüklenen, sıralı bir ızgarasını oluşturmanıza olanak tanıyan composable'lardır. LazyVerticalStaggeredGrid, öğelerini birden fazla sütuna yayılan ve her bir öğenin farklı yüksekliklerde olmasına olanak tanıyan, dikey olarak kaydırılabilir bir kapsayıcıda gösterir. Yatay tembel ızgaralar, farklı genişlikteki öğelerle yatay eksende aynı davranışa sahiptir.

Aşağıdaki snippet, öğe başına 200.dp genişliğe sahip LazyVerticalStaggeredGrid kullanımının temel bir örneğidir:

LazyVerticalStaggeredGrid(
    columns = StaggeredGridCells.Adaptive(200.dp),
    verticalItemSpacing = 4.dp,
    horizontalArrangement = Arrangement.spacedBy(4.dp),
    content = {
        items(randomSizedPhotos) { photo ->
            AsyncImage(
                model = photo,
                contentScale = ContentScale.Crop,
                contentDescription = null,
                modifier = Modifier
                    .fillMaxWidth()
                    .wrapContentHeight()
            )
        }
    },
    modifier = Modifier.fillMaxSize()
)

Şekil 1. Yavaş yüklenen kademeli dikey ızgara örneği

Sabit sayıda sütun ayarlamak için StaggeredGridCells.Adaptive yerine StaggeredGridCells.Fixed(columns) kullanabilirsiniz. Bu, kullanılabilir genişliği sütun sayısına (veya yatay bir ızgara için satır sayısına) böler ve her öğenin bu genişliği (veya yatay bir ızgara için yüksekliği) kaplamasını sağlar:

LazyVerticalStaggeredGrid(
    columns = StaggeredGridCells.Fixed(3),
    verticalItemSpacing = 4.dp,
    horizontalArrangement = Arrangement.spacedBy(4.dp),
    content = {
        items(randomSizedPhotos) { photo ->
            AsyncImage(
                model = photo,
                contentScale = ContentScale.Crop,
                contentDescription = null,
                modifier = Modifier
                    .fillMaxWidth()
                    .wrapContentHeight()
            )
        }
    },
    modifier = Modifier.fillMaxSize()
)
Compose'da resimlerin sıralı olmayan geç yüklenen ızgarası
Şekil 2. Sabit sütunlara sahip, kademeli dikey ızgara örneği

İçerik dolgusu

Bazen içeriğin kenarlarına dolgu eklemeniz gerekir. Lazy bileşenleri, bunu desteklemek için contentPadding parametresine bazı PaddingValues iletmenize olanak tanır:

LazyColumn(
    contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
) {
    // ...
}

Bu örnekte, yatay kenarlara (sol ve sağ) 16.dp, içeriğin üst ve alt kısmına ise 8.dp dolgu ekliyoruz.

Bu dolgunun LazyColumn öğesine değil, içeriğe uygulandığını unutmayın. Yukarıdaki örnekte, ilk öğenin üst kısmına 8.dp dolgu, son öğenin alt kısmına 8.dp dolgu eklenir ve tüm öğelerin sol ve sağ tarafında 16.dp dolgu olur.

Başka bir örnek olarak, Scaffold'nin PaddingValues değerini LazyColumn'nin contentPadding değerine aktarabilirsiniz. Uçtan uca kılavuzuna bakın.

İçerik aralığı

Öğeler arasına boşluk eklemek için Arrangement.spacedBy() kullanabilirsiniz. Aşağıdaki örnekte, her öğe arasına 4.dp boşluk eklenmektedir:

LazyColumn(
    verticalArrangement = Arrangement.spacedBy(4.dp),
) {
    // ...
}

Benzer şekilde, LazyRow için:

LazyRow(
    horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
    // ...
}

Ancak ızgaralar hem dikey hem de yatay düzenlemeleri kabul eder:

LazyVerticalGrid(
    columns = GridCells.Fixed(2),
    verticalArrangement = Arrangement.spacedBy(16.dp),
    horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
    items(photos) { item ->
        PhotoItem(item)
    }
}

Öğe anahtarları

Varsayılan olarak her öğenin durumu, listedeki veya tablodaki konumuna göre belirlenir. Ancak veri kümesi değişirse bu durum sorunlara neden olabilir. Çünkü konumu değişen öğeler, hatırlanan tüm durumları kaybeder. LazyColumn içinde LazyRow senaryosunu düşünürseniz satır, öğe konumunu değiştirirse kullanıcı satır içindeki kaydırma konumunu kaybeder.

.

Bunu önlemek için her öğeye kararlı ve benzersiz bir anahtar sağlayarak key parametresine bir blok sağlayabilirsiniz. Sabit bir anahtar sağlamak, öğe durumunun veri kümesi değişiklikleri genelinde tutarlı olmasını sağlar:

LazyColumn {
    items(
        items = messages,
        key = { message ->
            // Return a stable + unique key for the item
            message.id
        }
    ) { message ->
        MessageRow(message)
    }
}

Anahtar sağlayarak Compose'un yeniden sıralamaları doğru şekilde işlemesine yardımcı olursunuz. Örneğin, öğeniz hatırlanan durum içeriyorsa anahtarların ayarlanması, öğenin konumu değiştiğinde Compose'un bu durumu öğeyle birlikte taşımasına olanak tanır.

LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = remember {
            Random.nextInt()
        }
    }
}

Ancak öğe anahtarı olarak kullanabileceğiniz türlerle ilgili bir sınırlama vardır. Anahtarın türü, Etkinlik yeniden oluşturulduğunda durumları korumak için Android'in mekanizması olan Bundle tarafından desteklenmelidir. Bundle, temel türleri, enum'ları veya Parcelable'ları destekler.

LazyColumn {
    items(books, key = {
        // primitives, enums, Parcelable, etc.
    }) {
        // ...
    }
}

Anahtar, Bundle tarafından desteklenmelidir. Böylece, Etkinlik yeniden oluşturulduğunda veya bu öğeden uzaklaşıp tekrar geri kaydırdığınızda bile öğe bileşenindeki rememberSaveable geri yüklenebilir.

LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = rememberSaveable {
            Random.nextInt()
        }
    }
}

Öğe animasyonları

RecyclerView widget'ını kullandıysanız öğe değişikliklerini otomatik olarak animasyonlu hale getirdiğini bilirsiniz. Yavaş düzenler, öğe yeniden sıralamaları için aynı işlevi sağlar. API basittir. Öğe içeriğine animateItem değiştiricisini ayarlamanız yeterlidir:

LazyColumn {
    // It is important to provide a key to each item to ensure animateItem() works as expected.
    items(books, key = { it.id }) {
        Row(Modifier.animateItem()) {
            // ...
        }
    }
}

Gerekirse özel animasyon spesifikasyonu da sağlayabilirsiniz:

LazyColumn {
    items(books, key = { it.id }) {
        Row(
            Modifier.animateItem(
                fadeInSpec = tween(durationMillis = 250),
                fadeOutSpec = tween(durationMillis = 100),
                placementSpec = spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy)
            )
        ) {
            // ...
        }
    }
}

Taşınan öğenin yeni konumunu bulabilmek için öğeleriniz için anahtar sağladığınızdan emin olun.

Örnek: Tembel listelerdeki öğeleri canlandırma

Compose ile, geç listelerdeki öğelerde yapılan değişiklikleri animasyonlu hale getirebilirsiniz. Aşağıdaki snippet'ler birlikte kullanıldığında, geç listesi öğeleri eklenirken, kaldırılırken ve yeniden sıralanırken animasyonlar uygulanır.

Bu snippet, öğeler eklendiğinde, kaldırıldığında veya yeniden sıralandığında animasyonlu geçişler içeren bir dize listesi gösterir:

@Composable
fun ListAnimatedItems(
    items: List<String>,
    modifier: Modifier = Modifier
) {
    LazyColumn(modifier) {
        // Use a unique key per item, so that animations work as expected.
        items(items, key = { it }) {
            ListItem(
                headlineContent = { Text(it) },
                modifier = Modifier
                    .animateItem(
                        // Optionally add custom animation specs
                    )
                    .fillParentMaxWidth()
                    .padding(horizontal = 8.dp, vertical = 0.dp),
            )
        }
    }
}

Kodla ilgili önemli noktalar

  • ListAnimatedItems, öğeler değiştirildiğinde animasyonlu geçişler içeren bir LazyColumn içinde dize listesi gösterir.
  • items işlevi, listedeki her öğeye benzersiz bir anahtar atar. Compose, öğeleri izlemek ve konumlarındaki değişiklikleri belirlemek için anahtarları kullanır.
  • ListItem, her liste öğesinin düzenini tanımlar. Öğenin ana içeriğini tanımlayan bir headlineContent parametresi alır.
  • animateItem değiştiricisi; öğe ekleme, kaldırma ve taşıma işlemlerine varsayılan animasyonları uygular.

Aşağıdaki snippet'te, öğe ekleme ve kaldırma kontrollerinin yanı sıra önceden tanımlanmış bir listeyi sıralama kontrollerini içeren bir ekran gösterilmektedir:

@Composable
private fun ListAnimatedItemsExample(
    data: List<String>,
    modifier: Modifier = Modifier,
    onAddItem: () -> Unit = {},
    onRemoveItem: () -> Unit = {},
    resetOrder: () -> Unit = {},
    onSortAlphabetically: () -> Unit = {},
    onSortByLength: () -> Unit = {},
) {
    val canAddItem = data.size < 10
    val canRemoveItem = data.isNotEmpty()

    Scaffold(modifier) { paddingValues ->
        Column(
            modifier = Modifier
                .padding(paddingValues)
                .fillMaxSize()
        ) {
            // Buttons that change the value of displayedItems.
            AddRemoveButtons(canAddItem, canRemoveItem, onAddItem, onRemoveItem)
            OrderButtons(resetOrder, onSortAlphabetically, onSortByLength)

            // List that displays the values of displayedItems.
            ListAnimatedItems(data)
        }
    }
}

Kodla ilgili önemli noktalar

  • ListAnimatedItemsExample, öğe ekleme, kaldırma ve sıralama kontrollerini içeren bir ekran sunar.
    • onAddItem ve onRemoveItem, listeye öğe eklemek ve listeden öğe kaldırmak için AddRemoveButtons ifadesine iletilen lambda ifadeleridir.
    • resetOrder, onSortAlphabetically ve onSortByLength, listedeki öğelerin sırasını değiştirmek için OrderButtons işlevine iletilen lambda ifadeleridir.
  • AddRemoveButtons, "Ekle" ve "Kaldır" düğmelerini gösterir. Düğmeleri etkinleştirir/devre dışı bırakır ve düğme tıklamalarını yönetir.
  • OrderButtons, listeyi yeniden sıralama düğmelerini gösterir. Sırayı sıfırlamak ve listeyi uzunluğa veya alfabetik olarak sıralamak için lambda işlevlerini alır.
  • ListAnimatedItems, ListAnimatedItems composable'ını çağırır ve animasyonlu dize listesini görüntülemek için data listesini iletir. data başka bir yerde tanımlanmış.

Bu snippet, Öğe Ekle ve Öğeyi Sil düğmelerini içeren bir kullanıcı arayüzü oluşturur:

@Composable
private fun AddRemoveButtons(
    canAddItem: Boolean,
    canRemoveItem: Boolean,
    onAddItem: () -> Unit,
    onRemoveItem: () -> Unit
) {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.Center
    ) {
        Button(enabled = canAddItem, onClick = onAddItem) {
            Text("Add Item")
        }
        Spacer(modifier = Modifier.padding(25.dp))
        Button(enabled = canRemoveItem, onClick = onRemoveItem) {
            Text("Delete Item")
        }
    }
}

Kodla ilgili önemli noktalar

  • AddRemoveButtons, listede ekleme ve kaldırma işlemleri yapmak için bir düğme satırı gösterir.
  • canAddItem ve canRemoveItem parametreleri, düğmelerin etkinleştirilme durumunu kontrol eder. canAddItem veya canRemoveItem yanlışsa ilgili düğme devre dışı bırakılır.
  • onAddItem ve onRemoveItem parametreleri, kullanıcı ilgili düğmeyi tıkladığında yürütülen lambda'lardır.

Son olarak bu snippet, listeyi sıralamak için üç düğme (Sıfırla, Alfabetik ve Uzunluk) gösterir:

@Composable
private fun OrderButtons(
    resetOrder: () -> Unit,
    orderAlphabetically: () -> Unit,
    orderByLength: () -> Unit
) {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.Center
    ) {
        var selectedIndex by remember { mutableIntStateOf(0) }
        val options = listOf("Reset", "Alphabetical", "Length")

        SingleChoiceSegmentedButtonRow {
            options.forEachIndexed { index, label ->
                SegmentedButton(
                    shape = SegmentedButtonDefaults.itemShape(
                        index = index,
                        count = options.size
                    ),
                    onClick = {
                        Log.d("AnimatedOrderedList", "selectedIndex: $selectedIndex")
                        selectedIndex = index
                        when (options[selectedIndex]) {
                            "Reset" -> resetOrder()
                            "Alphabetical" -> orderAlphabetically()
                            "Length" -> orderByLength()
                        }
                    },
                    selected = index == selectedIndex
                ) {
                    Text(label)
                }
            }
        }
    }
}

Kodla ilgili önemli noktalar

  • OrderButtons, kullanıcıların listede bir sıralama yöntemi seçmesine veya liste sırasını sıfırlamasına olanak tanıyan bir SingleChoiceSegmentedButtonRow gösterir. SegmentedButton bileşeni, seçenek listesinden tek bir seçenek belirlemenize olanak tanır.
  • resetOrder, orderAlphabetically ve orderByLength, ilgili düğme seçildiğinde yürütülen lambda işlevleridir.
  • selectedIndex durum değişkeni, seçilen seçeneği takip eder.

Sonuç

Bu videoda, öğeler yeniden sıralandığında önceki snippet'lerin sonucu gösterilmektedir:

Şekil 1. Öğeler eklendiğinde, kaldırıldığında veya sıralandığında öğe geçişlerini animasyonlu hale getiren bir liste.

Yapışkan başlıklar

"Yapışkan başlık" deseni, gruplandırılmış veri listelerini gösterirken kullanışlıdır. Aşağıda, her bir kişinin adının baş harfine göre gruplandırılmış bir "kişiler listesi" örneği görebilirsiniz:

Kişiler listesinde yukarı ve aşağı kaydırılan bir telefonun videosu

LazyColumn ile yapışkan başlık oluşturmak için stickyHeader() kullanabilirsiniz.

işlevini kullanın:

@Composable
fun ListWithHeader(items: List<Item>) {
    LazyColumn {
        stickyHeader {
            Header()
        }

        items(items) { item ->
            ItemRow(item)
        }
    }
}

Yukarıdaki "kişiler listesi" örneğinde olduğu gibi birden fazla başlığı olan bir liste oluşturmak için:

// This ideally would be done in the ViewModel
val grouped = contacts.groupBy { it.firstName[0] }

@Composable
fun ContactsList(grouped: Map<Char, List<Contact>>) {
    LazyColumn {
        grouped.forEach { (initial, contactsForInitial) ->
            stickyHeader {
                CharacterHeader(initial)
            }

            items(contactsForInitial) { contact ->
                ContactListItem(contact)
            }
        }
    }
}

Kaydırma konumuna tepki verme

Birçok uygulamanın, kaydırma konumu ve öğe düzeni değişikliklerine tepki vermesi ve bunları dinlemesi gerekir. Lazy bileşenleri, LazyListState'yi yukarı çekerek bu kullanım alanını destekler:

@Composable
fun MessageList(messages: List<Message>) {
    // Remember our own LazyListState
    val listState = rememberLazyListState()

    // Provide it to LazyColumn
    LazyColumn(state = listState) {
        // ...
    }
}

Basit kullanım alanlarında uygulamaların genellikle yalnızca ilk görünür öğe hakkındaki bilgileri bilmesi gerekir. Bunun için LazyListState, firstVisibleItemIndex ve firstVisibleItemScrollOffset özelliklerini sağlar.

Kullanıcının ilk öğeyi geçip geçmediğine bağlı olarak bir düğmeyi gösterme ve gizleme örneğini kullanırsak:

@Composable
fun MessageList(messages: List<Message>) {
    Box {
        val listState = rememberLazyListState()

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

        // Show the button if the first visible item is past
        // the first item. We use a remembered derived state to
        // minimize unnecessary compositions
        val showButton by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex > 0
            }
        }

        AnimatedVisibility(visible = showButton) {
            ScrollToTopButton()
        }
    }
}

Durumu doğrudan oluşturma sırasında okumak, diğer kullanıcı arayüzü bileşenlerini güncellemeniz gerektiğinde kullanışlıdır ancak etkinliğin aynı oluşturma sırasında işlenmesi gerekmeyen senaryolar da vardır. Bunun yaygın bir örneği, kullanıcı belirli bir noktayı geçtikten sonra bir analiz etkinliği göndermektir. Bu sorunu verimli bir şekilde çözmek için snapshotFlow() kullanabiliriz:

val listState = rememberLazyListState()

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

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}

LazyListState, layoutInfo özelliği aracılığıyla o anda görüntülenen tüm öğeler ve bunların ekrandaki sınırları hakkında da bilgi sağlar. Daha fazla bilgi için LazyListLayoutInfo sınıfına bakın.

Kaydırma konumunu kontrol etme

Kaydırma konumuna tepki vermenin yanı sıra uygulamaların kaydırma konumunu kontrol edebilmesi de yararlıdır. LazyListState, kaydırma konumunu "hemen" yakalayan scrollToItem() işlevi ve animasyon kullanarak kaydıran (kolay kaydırma olarak da bilinir) animateScrollToItem() ile bunu destekler:

@Composable
fun MessageList(messages: List<Message>) {
    val listState = rememberLazyListState()
    // Remember a CoroutineScope to be able to launch
    val coroutineScope = rememberCoroutineScope()

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

    ScrollToTopButton(
        onClick = {
            coroutineScope.launch {
                // Animate scroll to the first item
                listState.animateScrollToItem(index = 0)
            }
        }
    )
}

Büyük veri kümeleri (sayfalama)

Paging kitaplığı, uygulamaların büyük öğe listelerini desteklemesini, listenin küçük parçalarını gerektiği gibi yükleyip görüntülemesini sağlar. Paging 3.0 ve sonraki sürümler, androidx.paging:paging-compose kitaplığı aracılığıyla Compose desteği sağlar.

Sayfalandırılmış içeriklerin listesini görüntülemek için collectAsLazyPagingItems() uzantı işlevini kullanabilir ve ardından döndürülen LazyPagingItems değerini LazyColumn içindeki items() öğesine iletebiliriz. Görünümlerdeki Sayfalandırma desteğine benzer şekilde, item değerinin null olup olmadığını kontrol ederek veriler yüklenirken yer tutucuları gösterebilirsiniz:

@Composable
fun MessageList(pager: Pager<Int, Message>) {
    val lazyPagingItems = pager.flow.collectAsLazyPagingItems()

    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it.id }
        ) { index ->
            val message = lazyPagingItems[index]
            if (message != null) {
                MessageRow(message)
            } else {
                MessagePlaceholder()
            }
        }
    }
}

Yavaş düzenleri kullanmayla ilgili ipuçları

Tembel düzenlerinizin amaçlandığı gibi çalışmasını sağlamak için dikkate alabileceğiniz birkaç ipucu vardır.

0 piksel boyutunda öğeler kullanmaktan kaçının

Bu durum, örneğin listenizin öğelerini daha sonraki bir aşamada doldurmak için resim gibi bazı verileri eşzamansız olarak almayı beklediğiniz senaryolarda ortaya çıkabilir. Bu, Lazy düzeninin ilk ölçümde tüm öğelerini oluşturmasına neden olur. Çünkü öğelerin yüksekliği 0 pikseldir ve tümü görüntü alanına sığabilir. Öğeler yüklendikten ve yükseklikleri genişledikten sonra Lazy düzenleri, aslında görüntü alanına sığamayacakları için ilk seferde gereksiz yere oluşturulan diğer tüm öğeleri siler. Bunu önlemek için öğelerinize varsayılan boyutlandırma ayarlamanız gerekir. Böylece, geç yüklenen düzen, görüntü alanına gerçekte kaç öğenin sığabileceğini doğru şekilde hesaplayabilir:

@Composable
fun Item(imageUrl: String) {
    AsyncImage(
        model = rememberAsyncImagePainter(model = imageUrl),
        modifier = Modifier.size(30.dp),
        contentDescription = null
        // ...
    )
}

Veriler eşzamansız olarak yüklendikten sonra öğelerinizin yaklaşık boyutunu biliyorsanız öğelerinizin boyutunun yüklemeden önce ve sonra aynı kalmasını sağlamak (ör. bazı yer tutucular ekleyerek) iyi bir uygulamadır. Bu, doğru kaydırma konumunun korunmasına yardımcı olur.

Aynı yönde kaydırılabilir bileşenleri iç içe yerleştirmekten kaçının.

Bu durum yalnızca, önceden tanımlanmış bir boyutu olmayan kaydırılabilir alt öğeleri aynı yönde kaydırılabilir başka bir üst öğenin içine yerleştirirken geçerlidir. Örneğin, sabit yüksekliği olmayan bir alt LazyColumn öğeyi dikey olarak kaydırılabilir bir üst Column öğenin içine yerleştirmeye çalışmak:

// throws IllegalStateException
Column(
    modifier = Modifier.verticalScroll(state)
) {
    LazyColumn {
        // ...
    }
}

Bunun yerine, tüm composable'larınızı tek bir üst LazyColumn içine sarmalayarak ve farklı içerik türlerini iletmek için DSL'sini kullanarak aynı sonucu elde edebilirsiniz. Bu sayede, tek bir öğenin yanı sıra birden fazla liste öğesini tek bir yerde yayınlayabilirsiniz:

LazyColumn {
    item {
        Header()
    }
    items(data) { item ->
        PhotoItem(item)
    }
    item {
        Footer()
    }
}

Farklı yön düzenlerini iç içe yerleştirdiğiniz durumlara (ör. kaydırılabilir üst öğe Row ve alt öğe LazyColumn) izin verildiğini unutmayın:

Row(
    modifier = Modifier.horizontalScroll(scrollState)
) {
    LazyColumn {
        // ...
    }
}

Ayrıca aynı yön düzenlerini kullanmaya devam ettiğiniz ancak iç içe yerleştirilmiş alt öğeler için sabit bir boyut da ayarladığınız durumlar:

Column(
    modifier = Modifier.verticalScroll(scrollState)
) {
    LazyColumn(
        modifier = Modifier.height(200.dp)
    ) {
        // ...
    }
}

Tek bir öğeye birden fazla öğe eklememeye dikkat edin

Bu örnekte, ikinci öğe lambda, bir blokta 2 öğe yayınlar:

LazyVerticalGrid(
    columns = GridCells.Adaptive(100.dp)
) {
    item { Item(0) }
    item {
        Item(1)
        Item(2)
    }
    item { Item(3) }
    // ...
}

Yavaş düzenler bunu beklendiği gibi işler. Öğeleri farklı öğelermiş gibi birbiri ardına yerleştirirler. Ancak bunu yapmanın birkaç sorunu vardır.

Birden fazla öğe tek bir öğenin parçası olarak yayınlandığında tek bir varlık olarak işlenir. Yani artık ayrı ayrı oluşturulamazlar. Ekranda bir öğe görünür hale gelirse öğeye karşılık gelen tüm öğelerin oluşturulması ve ölçülmesi gerekir. Aşırı kullanıldığında performansı olumsuz etkileyebilir. Tüm öğeleri tek bir öğeye yerleştirmek gibi uç bir durumda, Lazy düzenlerini kullanmanın amacı tamamen ortadan kalkar. Olası performans sorunlarının yanı sıra, bir öğeye daha fazla öğe eklemek scrollToItem() ve animateScrollToItem() ile de çakışır.

Ancak bir öğeye birden fazla öğe eklemenin geçerli kullanım alanları vardır. Örneğin, bir listeye ayırıcılar ekleyebilirsiniz. Ayırıcılar bağımsız öğeler olarak kabul edilmediğinden kaydırma dizinlerini değiştirmelerini istemezsiniz. Ayrıca, ayırıcılar küçük olduğu için performans etkilenmez. Bölücünün, kendisinden önceki öğe görünür olduğunda görünür olması gerekir. Böylece önceki öğenin bir parçası olabilir:

LazyVerticalGrid(
    columns = GridCells.Adaptive(100.dp)
) {
    item { Item(0) }
    item {
        Item(1)
        Divider()
    }
    item { Item(2) }
    // ...
}

Özel düzenlemeler kullanmayı düşünün

Genellikle Lazy listelerinde çok sayıda öğe bulunur ve bu öğeler, kaydırılabilir kapsayıcının boyutundan daha fazla yer kaplar. Ancak listenizde az sayıda öğe varsa tasarımınız, bu öğelerin görüntü alanında nasıl konumlandırılması gerektiğiyle ilgili daha spesifik gereksinimlere sahip olabilir.

Bunu yapmak için özel dikey Arrangement kullanabilir ve bunu LazyColumn öğesine iletebilirsiniz. Aşağıdaki örnekte, TopWithFooter nesnesinin yalnızca arrange yöntemini uygulaması gerekir. İlk olarak, öğeleri birbiri ardına yerleştirir. İkinci olarak, kullanılan toplam yükseklik, görüntü alanı yüksekliğinden daha düşükse altbilgiyi en alta yerleştirir:

object TopWithFooter : Arrangement.Vertical {
    override fun Density.arrange(
        totalSize: Int,
        sizes: IntArray,
        outPositions: IntArray
    ) {
        var y = 0
        sizes.forEachIndexed { index, size ->
            outPositions[index] = y
            y += size
        }
        if (y < totalSize) {
            val lastIndex = outPositions.lastIndex
            outPositions[lastIndex] = totalSize - sizes.last()
        }
    }
}

contentType ekleyebilirsiniz

Compose 1.2'den itibaren, Lazy düzeninizin performansını en üst düzeye çıkarmak için listelerinize veya ızgaralarınıza contentType ekleyebilirsiniz. Bu sayede, birden fazla farklı öğe türünden oluşan bir liste veya tablo oluşturduğunuz durumlarda, düzenin her bir öğesi için içerik türünü belirtebilirsiniz:

LazyColumn {
    items(elements, contentType = { it.type }) {
        // ...
    }
}

contentType sağladığınızda Compose, oluşturmaları yalnızca aynı türdeki öğeler arasında yeniden kullanabilir. Benzer yapıdaki öğeleri oluştururken yeniden kullanmak daha verimli olduğundan, içerik türlerini sağlamak Compose'un tamamen farklı bir B türü öğenin üzerine A türü bir öğe oluşturmaya çalışmamasını sağlar. Bu sayede, oluşturma yeniden kullanımının avantajlarından ve Lazy düzen performansınızdan en iyi şekilde yararlanabilirsiniz.

Performansı ölçme

Yavaş düzenin performansını yalnızca sürüm modunda ve R8 optimizasyonu etkinken güvenilir bir şekilde ölçebilirsiniz. Hata ayıklama derlemelerinde, Lazy düzeni kaydırma işlemi daha yavaş görünebilir. Bu konu hakkında daha fazla bilgi için Compose performansı başlıklı makaleyi okuyun.

Ek kaynaklar