Listeler ve ızgaralar

Birçok uygulamanın öğe koleksiyonlarını göstermesi gerekir. Bu belgede, 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 Column veya Row (yöne bağlı olarak) gibi basit bir öğe kullanabilir ve her öğenin içeriğini aşağıdaki şekilde bir listede 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.

Lazy lists

Çok sayıda öğe (veya uzunluğu bilinmeyen bir liste) göstermeniz 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 yerleştiren bir bileşen 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, 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, @Composable içerik blok parametresini kabul etmek ve uygulamaların doğrudan composable'lar yayınlamasına izin vermek yerine LazyListScope.() bloğu sağlar. Bu LazyListScope bloğu, 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 ayrıntılı bilgi için lütfen LazyListScope referansını inceleyin.

Lazy grids

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österirken LazyHorizontalGrid, yatay eksende aynı davranışı sergiler.

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

Fotoğrafların ızgara şeklinde gösterildiği 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. Kalan genişlik, sütun sayısı hesaplandıktan sonra 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 yararlı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 dışı boyutlara sahip olması gerekiyorsa öğeler için özel sütun aralıkları sağlamak üzere ızgara desteğini kullanabilirsiniz. span ve items yöntemlerinin span parametresiyle sütun aralığını belirtin.LazyGridScope DSLitem maxLineSpan, aralık kapsamının değerlerinden biri olup ö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")
    }
    // ...
}

Geç yüklenen kademeli ızgara

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

Aşağıdaki snippet, LazyVerticalStaggeredGrid öğesini 200.dp genişliğinde kullanmanı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. Lazy staggered vertical grid ö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()
)
Oluşturma modunda resimlerin geç yüklenen kademeli ızgarası
Şekil 2. Sabit sütunlara sahip, 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 PaddingValues öğelerini contentPadding parametresine 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 değerini LazyColumn'nın contentPadding değerine iletebilirsiniz. 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),
) {
    // ...
}

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 tablodaki 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ının satır içindeki kaydırma konumu kaybolur.

Bununla mücadele etmek için her öğe için sabit ve benzersiz bir anahtar sağlayarak key parametresini engelleyebilirsiniz. Kararlı 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)
    }
}

Anahtarlar sağlayarak Compose'un yeniden sıralamaları doğru şekilde işlemesine yardımcı olursunuz. Örneğin, öğeniz hatırlanan durumu içeriyorsa anahtarları ayarlamak, Compose'un 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ü, Etkinlik yeniden oluşturulduğunda durumları korumak için Android'in kullandığı mekanizma olan Bundle tarafından desteklenmelidir. Bundle, ilkel 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şlevselliği sağlar. API basittir. Yalnızca animateItem değiştiricisini öğe içeriğine ayarlamanız gerekir:

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()) {
            // ...
        }
    }
}

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

Örnek: Geç yüklenen listelerdeki öğeleri canlandırma

Compose ile, tembel listelerdeki öğelerde yapılan değişiklikleri canlandırabilirsiniz. Aşağıdaki snippet'ler birlikte kullanıldığında, tembel 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 dizi dize 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 gösterir.
    • onAddItem ve onRemoveItem, listeden öğe eklemek ve kaldırmak için AddRemoveButtons'ye 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ı işler.
  • OrderButtons, listenin yeniden sıralanması için düğmeleri 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ğırarak data listesini geçirir ve animasyonlu dize listesini gösterir. 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 etkin 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 lambdalardır.

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

@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, bir seçenek listesinden tek bir seçenek belirlemenizi sağlar.
  • 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:

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

Sabit başlıklar (deneysel)

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

Telefonun kişi listesinde yukarı ve aşağı kaydırıldığı video

LazyColumn ile sabit bir başlık elde etmek için deneysel stickyHeader() işlevini kullanabilir ve başlık içeriğini sağlayabilirsiniz:

@OptIn(ExperimentalFoundationApi::class)
@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 üstbilgi içeren bir liste oluşturmak için şunları yapabilirsiniz:

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

@OptIn(ExperimentalFoundationApi::class)
@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ında bilgi sahibi olması 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ıdır. Ancak etkinliğin aynı kompozisyonda 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 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 göz atı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() (aynı zamanda sorunsuz kaydırma olarak da bilinir) aracılığıyla 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 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()'ye iletebiliriz. Görünümlerdeki sayfalama 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'u 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 yaşanabilir. Bu durumda, Lazy düzen, tüm öğelerinin yüksekliği 0 piksel olduğundan ve hepsini görüntü alanına sığdırabileceğinden, tüm öğelerini ilk ölçümde oluşturur. Öğeler yüklendikten ve yükseklikleri genişletildikten sonra Lazy düzenler, ilk seferde gereksiz yere oluşturulan diğer tüm öğeleri görüntü alanına sığamayacakları için atar. Bunu önlemek için, Lazy düzeninin görüntü alanına kaç öğenin sığabileceğiyle ilgili doğru hesaplamayı yapabilmesi için öğelerinizde varsayılan boyutlandırmayı ayarlamanız gerekir:

@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 üst öğenin Column içine sabit yüksekliği olmayan bir alt öğe LazyColumn 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, tek öğelerin ve birden fazla liste öğesinin tek bir yerden yayınlanmasını sağlar:

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 yaparken dikkat etmeniz gereken birkaç nokta vardır.

Birden fazla öğe tek bir öğenin parçası olarak yayınlandığında tek bir varlık olarak ele alınır. Bu nedenle, 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. Bu özellik aşırı kullanıldığında performansı olumsuz etkileyebilir. Tüm öğeleri tek bir öğeye yerleştirme gibi uç durumlarda, tembel düzenlerin kullanılma amacı tamamen ortadan kalkar. Tek bir öğeye daha fazla öğe eklemek, olası performans sorunlarının yanı sıra scrollToItem() ve animateScrollToItem() ile de etkileşimi engeller.

Ancak, bir öğeye birden fazla öğe yerleştirmenin geçerli kullanım alanları vardır. Örneğin, bir listenin içinde ayırıcılar kullanmak. Bölücülerin bağımsız öğeler olarak değerlendirilmemesi gerektiğinden kaydırma dizinlerini değiştirmesini istemiyorsunuz. 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. Bu nedenle, ayırıcılar ö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 sektör Arrangement kullanabilir ve bunu LazyColumn'ye iletebilirsiniz. Aşağıdaki örnekte, TopWithFooter nesnesinin yalnızca arrange yöntemini uygulaması gerekir. İlk olarak, öğeleri art arda konumlandırır. İ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 eklemeyi düşünebilirsiniz.

Compose 1.2'den itibaren, Lazy düzeninizin performansını en üst düzeye çıkarmak için listelerinize veya ızgaralarınıza contentType eklemeyi düşünebilirsiniz. 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, Compose 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 tamamen farklı bir B türündeki öğ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

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üzeninde kaydırma daha yavaş görünebilir. Bu konu hakkında daha fazla bilgi için Oluşturma performansı başlıklı makaleyi inceleyin.

Ek kaynaklar