Lazy listeler ve lazy ızgaralar

Birçok uygulamanın öğe koleksiyonlarını göstermesi gerekir. Bu belgede, Jetpack Compose'da bu işlemi nasıl verimli bir şekilde yapabileceğiniz açıklanmaktadır.

Kullanım alanınızda kaydırma gerekmediğini biliyorsanız Column veya Row (yöne bağlı olarak) gibi basit bir kaydırma çubuğu kullanabilir ve aşağıdaki şekilde bir listede yineleme yaparak her öğenin içeriğini 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.

Lazy lists

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

Compose, yalnızca bileşenin görüntü alanında görünür olan öğeleri oluşturan ve düzenleyen bir bileşenler grubu sağlar. Bu bileşenler arasında LazyColumn ve LazyRow yer alır.

Adından da anlaşılacağı gibi, LazyColumn ve LazyRow arasındaki fark, öğelerini yerleştirme ve kaydırma yönüdür. LazyColumn dikey olarak kaydırılan bir liste oluşturur ve LazyRow 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 izin veren bir @Composable içerik bloğu parametresini kabul etmek yerine LazyListScope.() bloğu sağlar. Bu LazyListScope blok, uygulamaların öğe içeriklerini açıklamasına olanak tanıyan bir DSL sunar. Lazy bileşeni, düzen ve kaydırma konumunun gerektirdiği şekilde her öğenin içeriğini eklemekten sorumludur.

LazyListScope DSL

LazyListScope 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 çeşitli uzantı işlevleri de vardır. Bu uzantılar, yukarıdaki Column örneğini kolayca taşımamıza olanak tanır:

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

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

Geç yüklenen ızgaralar

LazyVerticalGrid ve LazyHorizontalGrid Composable'ları, öğelerin ızgarada gösterilmesini destekler. LazyVerticalGrid, öğelerini dikey olarak kaydırılabilir bir kapsayıcıda, birden fazla sütuna yayılmış şekilde gösterir. LazyHorizontalGrid ise yatay eksende aynı davranışı sergiler.

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

Fotoğraf ızgarası gösteren bir telefonun ekran 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 en az 128.dp genişliğinde olması 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östermek 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 DSL span parametresiyle sütun aralığını belirtin. maxLineSpan, aralık kapsamının değerlerinden biri olarak özellikle uyarlanabilir boyutlandırma kullanırken yararlıdır. Bunun nedeni, sütun sayısının sabit olmaması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")
    }
    // ...
}

Tembelce kademelendirilmiş ızgara

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

Aşağıdaki snippet, LazyVerticalStaggeredGrid öğesini 200.dp genişliğinde kullanarak oluşturulmuş temel bir örnektir:

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ş 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 tembel yüklenen kademeli ızgarası
Şekil 2. Sabit sütunlu, tembel yüklenen, kademeli dikey ızgara örneği

İçerik dolgusu

Bazen içeriğin kenarlarına dolgu eklemeniz gerekir. Lazy bileşenler, bunu desteklemek için contentPadding parametresine bazı PaddingValues değerlerini 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 için değil, içerik için uygulandığını lütfen 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ğ kısımlarında 16.dp dolgu bulunur.

Başka bir örnek olarak, Scaffold'nın PaddingValues'sini LazyColumn'nın contentPadding'üne iletebilirsiniz. Uçtan uca kılavuzuna bakın.

İçerik aralığı

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

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

LazyRow için de benzer şekilde:

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 ızgaradaki konumuna göre belirlenir. Ancak bu durum, veri kümesi değiştiğinde sorunlara neden olabilir. Çünkü konumu değişen öğeler, hatırlanan durumlarını kaybeder. LazyRow içinde LazyColumn senaryosunu düşünürsek satır öğe konumunu değiştirirse kullanıcı satır içindeki kaydırma konumunu kaybeder.

Bununla mücadele etmek için her öğe için sabit ve benzersiz bir anahtar sağlayarak key parametresine bir blok ekleyebilirsiniz. Sabit bir anahtar sağlamak, öğe durumunun veri kümesi değişiklikleri boyunca tutarlı olmasını sağlar:

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

Anahtarlar sağlayarak Compose'un yeniden sıralamaları doğru şekilde işlemesine yardımcı olursunuz. Örneğin, öğenizde hatırlanan durum varsa ayar anahtarları, Compose'un öğenin konumu değiştiğinde 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ü, Android'in Etkinlik yeniden oluşturulduğunda durumları korumaya yönelik mekanizması olan Bundle tarafından desteklenmelidir. Bundle; temel türler, numaralandırmalar veya Parcelable'lar gibi türleri destekler.

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

Öğenin composable'ı içindeki rememberSaveable, etkinlik yeniden oluşturulduğunda veya bu öğeden uzaklaşıp geri kaydırdığınızda geri yüklenebilmesi için anahtarın Bundle tarafından desteklenmesi gerekir.

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 animasyonla gösterdiğini bilirsiniz. Lazy düzenler, öğelerin yeniden sıralanması için aynı işlevleri sağlar. API basittir. Tek yapmanız gereken animateItem değiştiricisini öğe içeriğine ayarlamaktır:

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 bile 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 konumunun bulunabilmesi için öğelerinizin anahtarlarını sağladığınızdan emin olun.

Örnek: Geç yüklenen listelerdeki öğeleri animasyonla gösterme

Compose ile, tembel listelerdeki öğelerde yapılan değişiklikleri animasyonlu hale getirebilirsiniz. Aşağıdaki snippet'ler birlikte kullanıldığında, geç yüklenen liste öğ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 dizeler listesi görüntüler:

@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şlerle LazyColumn içinde bir 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 denetimlerini içeren bir ekran gösterir.
    • onAddItem ve onRemoveItem, listeden öğe eklemek ve kaldırmak için AddRemoveButtons'a iletilen lambda ifadeleridir.
    • resetOrder, onSortAlphabetically ve onSortByLength, listedeki öğelerin sırasını değiştirmek için OrderButtons'ye iletilen lambda ifadeleridir.
  • AddRemoveButtons simgesinde "Ekle" ve "Kaldır" düğmeleri gösterilir. 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ıralamayı sıfırlamak ve listeyi uzunluğa veya alfabetik olarak sıralamak için lambda işlevlerini alır.
  • ListAnimatedItems, ListAnimatedItems composable'ını çağırarak data listesini iletir ve animasyonlu dizeler listesini görüntüler. data başka bir yerde tanımlanmışsa.

Bu snippet, Öğe Ekle ve Öğe Sil düğmelerinin bulunduğu 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ştirilmiş durumunu kontrol eder. canAddItem veya canRemoveItem yanlış değerine ayarlanırsa 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ımak için SingleChoiceSegmentedButtonRow simgesini gösterir. A SegmentedButton bileşeni, seçenekler 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 canlandıran bir liste.

Yapışkan üstbilgiler

"Sabit başlık" kalıbı, gruplandırılmış veri listelerini görüntülerken faydalıdır. Aşağıda, her bir kişinin adının ilk harfine göre gruplandırılmış bir "kişi listesi" örneğini görebilirsiniz:

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

LazyColumn ile sabit üstbilgi elde etmek için stickyHeader()

işlevini kullanarak üstbilgi içeriğini sağlayın:

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

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

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

// 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 konumuna ve öğe düzeni değişikliklerine tepki vermesi ve bunları dinlemesi gerekir. Lazy bileşenleri, LazyListState öğesini yükselterek 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) {
        // ...
    }
}

Uygulamaların basit kullanım alanlarında genellikle yalnızca ilk görünür öğe hakkındaki bilgileri bilmesi gerekir. Bu durumda 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 kompozisyonda okumak, diğer kullanıcı arayüzü composable'larını güncellemeniz gerektiğinde yararlı olur. Ancak etkinliğin aynı kompozisyonda işlenmesi gerekmediği senaryolar da vardır. Bunun yaygın bir örneği, kullanıcı belirli bir noktayı geçtikten sonra bir Analytics etkinliği göndermektir. Bu durumu verimli bir şekilde ele almak 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, şu anda görüntülenen tüm öğeler ve 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

Uygulamaların kaydırma konumuna tepki vermesinin yanı sıra kaydırma konumunu kontrol edebilmesi de yararlıdır. LazyListState, kaydırma konumunu "anında" sabitleyen scrollToItem() işlevi ve animasyon kullanarak kaydıran animateScrollToItem() (düzgün kaydırma olarak da bilinir) aracılığıyla bunu destekler:

coroutines dokümanlarımıza göz atın.

@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 gerektiğinde listenin küçük parçalarını yükleyip görüntüleyerek büyük öğe listelerini desteklemesini sağlar. Paging 3.0 ve sonraki sürümler, androidx.paging:paging-compose kitaplığı aracılığıyla Compose desteği sunar.

Sayfalandırılmış içeriklerin listesini göstermek için collectAsLazyPagingItems() uzantı işlevini kullanabiliriz. 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 özelliğinin 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()
            }
        }
    }
}

Lazy layouts kullanmayla ilgili ipuçları

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

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

Örneğin, listenizdeki öğeleri daha sonra doldurmak için resim gibi bazı verileri eşzamansız olarak almayı beklediğiniz senaryolarda bu durum oluşabilir. Bu durumda, Lazy düzeni tüm öğelerini ilk ölçümde oluşturur. Bunun nedeni, öğelerin yüksekliğinin 0 piksel olması ve hepsinin görüntü alanına sığabilmesidir. Öğeler yüklendikten ve yükseklikleri genişletildikten sonra, Lazy düzenler, ilk seferde gereksiz yere oluşturulmuş tüm diğer öğeleri, görünüm alanına sığamayacakları için atar. Bunu önlemek için öğelerinizde varsayılan boyutlandırma ayarlamanız gerekir. Böylece Lazy düzeni, görünüm alanına kaç öğenin sığabileceğiyle ilgili doğru hesaplamayı yapabilir:

@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 iyi bir uygulamadır. Örneğin, bazı yer tutucular ekleyebilirsiniz. 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 yalnızca, önceden tanımlanmış bir boyutu olmayan kaydırılabilir alt öğelerin aynı yönde kaydırılabilir başka bir üst öğenin içine yerleştirildiği durumlarda geçerlidir. Örneğin, dikey olarak kaydırılabilir bir Column üst öğenin içine sabit yüksekliği olmayan bir LazyColumn alt öğe yerleştirmeye çalışmak:

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

Bunun yerine, tüm composable'larınızı tek bir üst öğe LazyColumn içine sarmalayarak ve farklı içerik türlerini iletmek için bu üst öğenin DSL'sini kullanarak aynı sonuç elde edilebilir. Bu sayede tek öğelerin yanı sıra birden fazla liste öğesi de tek bir yerde yayınlanabilir:

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

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

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

Aynı yön düzenlerini kullanmaya devam ettiğiniz ancak iç içe yerleştirilmiş alt öğelere 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 yerleştirmemeye dikkat edin.

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

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

Lazy düzenler bunu beklendiği gibi ele alır. Öğeleri, farklı öğelermiş gibi art arda düzenler. Ancak bunu yapmanın birkaç sorunu vardır.

Bir öğe kapsamında birden fazla öğe yayınlandığında bunlar tek bir varlık olarak işlenir. Bu nedenle, artık ayrı ayrı oluşturulamazlar. Ekranda bir öğe görünür hale gelirse öğeye karşılık gelen tüm öğeler oluşturulup ölçülmelidir. Bu özellik aşırı kullanıldığında performansı olumsuz etkileyebilir. Tüm öğeleri tek bir öğeye yerleştirme gibi uç durumlarda, Lazy düzenleri kullanmanın amacı tamamen ortadan kalkar. Tek bir öğeye daha fazla öğe yerleştirmek, olası performans sorunlarının yanı sıra scrollToItem() ve animateScrollToItem() ile de etkileşimi engeller.

Ancak, bir öğeye birden fazla öğe yerleştirmenin (ör. bir listenin içinde ayırıcılar kullanma) geçerli kullanım alanları vardır. Ayırıcılar bağımsız öğeler olarak kabul edilmemeleri gerektiğinden kaydırma dizinlerini değiştirmemelidir. Ayrıca, ayırıcılar küçük olduğundan performans etkilenmez. Önündeki öğe görünür olduğunda ayırıcının da görünür olması gerekir. Böylece ayırıcı, önceki öğenin bir parçası olabilir:

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

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

Genellikle tembel listelerde çok sayıda öğe bulunur ve bunlar kaydırma kapsayıcısının boyutundan daha fazla yer kaplar. Ancak listeniz az sayıda öğeyle doldurulduğunda tasarımınız, bu öğelerin görünüm alanında nasıl konumlandırılması gerektiği konusunda daha spesifik gereksinimlere sahip olabilir.

Bunu yapmak için özel dikey Arrangement kullanabilir ve LazyColumn'ye iletebilirsiniz. Aşağıdaki örnekte, TopWithFooter nesnesinin yalnızca arrange yöntemini uygulaması gerekir. İlk olarak, öğeleri art arda yerleştirir. İkinci olarak, kullanılan toplam yükseklik, görünüm alanı yüksekliğinden düşükse altbilgi en altta konumlandırılır:

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ı türde öğeden oluşan bir liste veya ızgara oluşturduğunuz durumlarda düzenin her öğesi için içerik türünü belirtebilirsiniz:

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

contentType öğesini sağladığınızda, Oluşturma yalnızca aynı türdeki öğeler arasında kompozisyonları yeniden kullanabilir. Benzer yapıdaki öğeleri oluştururken yeniden kullanmak daha verimli olduğundan, içerik türlerini sağlamak Oluşturma'nın A türündeki bir öğeyi B türündeki tamamen farklı bir öğenin üzerine oluşturmaya çalışmamasını sağlar. Bu, kompozisyonu yeniden kullanmanın ve Lazy düzen performansınızın avantajlarını en üst düzeye çıkarmanıza yardımcı olur.

Performansı ölçme

Bir Lazy düzeninin performansını yalnızca yayın modunda çalıştırırken ve R8 optimizasyonu etkinleştirilmişken güvenilir bir şekilde ölçebilirsiniz. Hata ayıklama derlemelerinde, Lazy düzen kaydırma işlemi daha yavaş görünebilir. Bu konu hakkında daha fazla bilgi edinmek için Oluşturma performansı başlıklı makaleyi inceleyin.

Ek kaynaklar