รายการและตารางกริดแบบเลซี

แอปจำนวนมากจำเป็นต้องแสดงคอลเล็กชันของรายการต่างๆ เอกสารนี้อธิบายวิธีดำเนินการนี้อย่างมีประสิทธิภาพใน Jetpack Compose

หากทราบว่ากรณีการใช้งานของคุณไม่จำเป็นต้องเลื่อน คุณอาจต้องการ ใช้ Column หรือ Row อย่างง่าย (ขึ้นอยู่กับทิศทาง) และส่งเนื้อหาของแต่ละรายการโดย วนซ้ำในรายการด้วยวิธีต่อไปนี้

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

เราทำให้ Column เลื่อนได้โดยใช้ตัวแก้ไข verticalScroll()

รายการแบบ Lazy

หากคุณต้องการแสดงรายการจำนวนมาก (หรือรายการที่มีความยาวที่ไม่รู้จัก) การใช้เลย์เอาต์ เช่น Column อาจทำให้เกิดปัญหาด้านประสิทธิภาพ เนื่องจากระบบจะจัดวางและแสดงรายการทั้งหมดไม่ว่าจะมองเห็นหรือไม่ก็ตาม

Compose มีชุดคอมโพเนนต์ที่จัดองค์ประกอบและวางรายการที่มองเห็นได้ในวิวพอร์ตของคอมโพเนนต์เท่านั้น โดยคอมโพเนนต์เหล่านี้ประกอบด้วย LazyColumn และ LazyRow

ตามชื่อที่ระบุ ความแตกต่างระหว่าง LazyColumn กับ LazyRow คือการวางแนวรายการและการเลื่อน LazyColumn จะสร้างรายการแบบเลื่อนในแนวตั้ง และ LazyRow จะสร้างรายการแบบเลื่อนในแนวนอน

คอมโพเนนต์ Lazy จะแตกต่างจากเลย์เอาต์ส่วนใหญ่ใน Compose แทนที่จะยอมรับ@Composableพารามิเตอร์การบล็อกเนื้อหา ซึ่งอนุญาตให้แอปปล่อย Composable โดยตรง คอมโพเนนต์ Lazy จะมีLazyListScope.()บล็อก บล็อก LazyListScope นี้มี DSL ที่ช่วยให้แอปอธิบายเนื้อหาของรายการได้ จากนั้นคอมโพเนนต์ Lazy จะมีหน้าที่เพิ่มเนื้อหาของแต่ละรายการตามที่เลย์เอาต์และตำแหน่งการเลื่อนกำหนด

LazyListScope DSL

DSL ของ LazyListScope มีฟังก์ชันหลายอย่างสำหรับอธิบายรายการ ในเลย์เอาต์ โดยพื้นฐานแล้ว item() จะเพิ่มรายการเดียว และ items(Int) จะเพิ่มหลายรายการ

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")
    }
}

นอกจากนี้ยังมีฟังก์ชันส่วนขยายหลายอย่างที่ช่วยให้คุณเพิ่มคอลเล็กชันของรายการต่างๆ ได้ เช่น List ส่วนขยายเหล่านี้ช่วยให้เราย้ายข้อมูลตัวอย่าง Column จากด้านบนได้อย่างง่ายดาย

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

นอกจากนี้ยังมีฟังก์ชันส่วนขยาย items() อีกเวอร์ชันหนึ่งชื่อ itemsIndexed() ซึ่งจะแสดงดัชนี โปรดดูรายละเอียดเพิ่มเติมในข้อมูลอ้างอิงของ LazyListScope

ตารางกริดแบบ Lazy

ฟังก์ชันที่ประกอบกันได้ LazyVerticalGrid และ LazyHorizontalGrid รองรับการแสดงรายการในตารางกริด Lazy vertical grid จะแสดงรายการในคอนเทนเนอร์ที่เลื่อนได้ในแนวตั้ง ซึ่งครอบคลุม หลายคอลัมน์ ส่วน Lazy horizontal grid จะมีลักษณะการทำงานเดียวกัน ในแกนแนวนอน

กริดมีความสามารถของ API ที่มีประสิทธิภาพเช่นเดียวกับลิสต์ และยังใช้ DSL ที่คล้ายกันมาก LazyGridScope.() ในการอธิบายเนื้อหา

ภาพหน้าจอโทรศัพท์ที่แสดงตารางกริดรูปภาพ

พารามิเตอร์ columns ใน LazyVerticalGrid และพารามิเตอร์ rows ใน LazyHorizontalGrid จะควบคุมวิธีสร้างเซลล์เป็นคอลัมน์หรือแถว ตัวอย่างต่อไปนี้แสดงรายการในตารางกริดโดยใช้ GridCells.Adaptive เพื่อตั้งค่าให้แต่ละคอลัมน์มีความกว้างอย่างน้อย 128.dp

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

LazyVerticalGrid ช่วยให้คุณระบุความกว้างของรายการ แล้วกริดจะ จัดคอลัมน์ให้ได้มากที่สุด ระบบจะกระจายความกว้างที่เหลือเท่าๆ กัน ในคอลัมน์ต่างๆ หลังจากคำนวณจำนวนคอลัมน์แล้ว การปรับขนาดในลักษณะนี้มีประโยชน์อย่างยิ่งสำหรับการแสดงชุดสินค้า ในหน้าจอขนาดต่างๆ

หากทราบจำนวนคอลัมน์ที่แน่นอนที่จะใช้ คุณสามารถระบุอินสแตนซ์ของ GridCells.Fixed ที่มีจำนวนคอลัมน์ที่จำเป็นแทนได้

หากการออกแบบของคุณต้องการให้มีเฉพาะบางรายการที่มีขนาดที่ไม่เป็นมาตรฐาน คุณสามารถใช้การรองรับกริดเพื่อระบุช่วงคอลัมน์ที่กำหนดเองสำหรับรายการได้ ระบุช่วงคอลัมน์ด้วยพารามิเตอร์ span ของเมธอด LazyGridScope DSL item และ items maxLineSpan ซึ่งเป็นค่าหนึ่งของขอบเขตสแปน มีประโยชน์อย่างยิ่งเมื่อคุณใช้ การปรับขนาดแบบอิงตามเนื้อหา เนื่องจากจำนวนคอลัมน์ไม่ได้คงที่ ตัวอย่างนี้แสดงวิธีระบุช่วงแถวแบบเต็ม

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

Lazy staggered grid

LazyVerticalStaggeredGrid และ LazyHorizontalStaggeredGrid คือ Composable ที่ช่วยให้คุณสร้างตารางกริดแบบเหลื่อมของรายการที่โหลดแบบ Lazy ได้ LazyVerticalStaggeredGrid จะแสดงรายการในคอนเทนเนอร์ที่เลื่อนได้ในแนวตั้งซึ่งครอบคลุมหลายคอลัมน์และอนุญาตให้แต่ละรายการมีความสูงแตกต่างกัน Lazy horizontal grids have the same behavior on the horizontal axis with items of different widths.

ข้อมูลโค้ดต่อไปนี้เป็นตัวอย่างพื้นฐานของการใช้ LazyVerticalStaggeredGrid ที่มีความกว้าง 200.dp ต่อรายการ

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()
)

รูปที่ 1 ตัวอย่างตารางกริดแนวตั้งแบบสลับที่โหลดแบบ Lazy

หากต้องการตั้งค่าจำนวนคอลัมน์คงที่ คุณสามารถใช้ StaggeredGridCells.Fixed(columns) แทน StaggeredGridCells.Adaptive ได้ ซึ่งจะแบ่งความกว้างที่มีตามจำนวนคอลัมน์ (หรือแถวสำหรับกริดแนวนอน) และกำหนดให้แต่ละรายการใช้ความกว้างนั้น (หรือความสูงสำหรับกริดแนวนอน) ดังนี้

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()
)
ตารางกริดแบบสลับรูปภาพที่โหลดแบบ Lazy Loading ใน Compose
รูปที่ 2 ตัวอย่างเลย์เอาต์กริดแนวตั้งแบบสลับที่โหลดแบบ Lazy Loading โดยมีคอลัมน์คงที่

ระยะห่างจากขอบเนื้อหา

บางครั้งคุณอาจต้องเพิ่มระยะขอบรอบขอบของเนื้อหา Lazy components ช่วยให้คุณส่งPaddingValues บางรายการไปยังพารามิเตอร์ contentPadding เพื่อรองรับการดำเนินการนี้ได้

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

ในตัวอย่างนี้ เราจะเพิ่มระยะขอบ 16.dp ที่ขอบแนวนอน (ซ้ายและขวา) แล้วเพิ่ม 8.dp ที่ด้านบนและด้านล่างของเนื้อหา

โปรดทราบว่าการเว้นวรรคนี้ใช้กับเนื้อหา ไม่ใช่กับ LazyColumn เอง ในตัวอย่างด้านบน รายการแรกจะเพิ่ม 8.dp ระยะห่างจากขอบด้านบน รายการสุดท้ายจะเพิ่ม 8.dp ที่ด้านล่าง และรายการทั้งหมด จะมีระยะห่างจากขอบ 16.dp ด้านซ้ายและขวา

อีกตัวอย่างหนึ่งคือ คุณสามารถส่ง Scaffold ของ PaddingValues ไปยัง contentPadding ของ LazyColumn ได้ ดูคำแนะนำเกี่ยวกับขอบถึงขอบ

การเว้นวรรคเนื้อหา

หากต้องการเพิ่มระยะห่างระหว่างรายการ คุณสามารถใช้ Arrangement.spacedBy() ตัวอย่างด้านล่างจะเพิ่มช่องว่าง 4.dp ระหว่างแต่ละรายการ

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

เช่นเดียวกับ LazyRow

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

อย่างไรก็ตาม กริดจะยอมรับทั้งการจัดเรียงแนวตั้งและแนวนอน

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

คีย์รายการ

โดยค่าเริ่มต้น สถานะของแต่ละรายการจะเชื่อมโยงกับตำแหน่งของรายการใน ลิสต์หรือตารางกริด อย่างไรก็ตาม การดำเนินการนี้อาจทำให้เกิดปัญหาหากชุดข้อมูลเปลี่ยนแปลง เนื่องจาก รายการที่เปลี่ยนตำแหน่งจะสูญเสียสถานะที่จดจำไว้ หากคุณนึกถึงสถานการณ์ของ LazyRow ภายใน LazyColumn หากแถวเปลี่ยนตำแหน่งของรายการ ผู้ใช้จะสูญเสียตำแหน่งการเลื่อนภายในแถวนั้น

คุณสามารถระบุคีย์ที่ไม่ซ้ำกันและเสถียรสำหรับแต่ละรายการเพื่อป้องกันปัญหานี้ได้ โดยระบุบล็อกให้กับพารามิเตอร์ key การระบุคีย์ที่เสถียรจะช่วยให้สถานะของรายการสอดคล้องกันเมื่อมีการเปลี่ยนแปลงชุดข้อมูล

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

การระบุคีย์จะช่วยให้ Compose จัดการการเรียงลำดับใหม่ได้อย่างถูกต้อง เช่น หากรายการมีสถานะที่จดจำได้ การตั้งค่าคีย์จะช่วยให้ Compose ย้ายสถานะนี้ไปพร้อมกับรายการได้เมื่อมีการเปลี่ยนตำแหน่ง

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

อย่างไรก็ตาม มีข้อจำกัดหนึ่งเกี่ยวกับประเภทที่คุณใช้เป็นคีย์รายการได้ Bundle ซึ่งเป็นกลไกของ Android สำหรับรักษาสถานะเมื่อสร้าง Activity ขึ้นใหม่ต้องรองรับประเภทของคีย์ Bundle รองรับประเภทต่างๆ เช่น ประเภทดั้งเดิม การแจงนับ หรือ Parcelable

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

Bundle ต้องรองรับคีย์เพื่อให้สามารถกู้คืน rememberSaveable ภายใน รายการที่ใช้ร่วมกันได้เมื่อสร้าง Activity ใหม่ หรือแม้แต่ เมื่อคุณเลื่อนออกจากรายการนี้แล้วเลื่อนกลับมา

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

ภาพเคลื่อนไหวของรายการ

หากเคยใช้เครื่องมือ RecyclerView คุณจะทราบว่าเครื่องมือนี้จะเปลี่ยนรายการ ภาพเคลื่อนไหวโดยอัตโนมัติ เลย์เอาต์แบบเลซี่มีฟังก์ชันการทำงานเดียวกันสำหรับการจัดเรียงรายการใหม่ API ใช้งานง่าย เพียงแค่ตั้งค่าตัวแก้ไข animateItem ให้กับเนื้อหารายการ

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

คุณยังระบุภาพเคลื่อนไหวที่กำหนดเองได้หากต้องการ

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

ตรวจสอบว่าคุณระบุคีย์สำหรับรายการต่างๆ เพื่อให้ค้นหาตำแหน่งใหม่ ขององค์ประกอบที่ย้ายได้

ตัวอย่าง: ทำให้รายการในรายการที่โหลดแบบ Lazy โหลดเคลื่อนไหว

Compose ช่วยให้คุณเคลื่อนไหวการเปลี่ยนแปลงรายการใน Lazy List ได้ เมื่อใช้ร่วมกัน ข้อมูลโค้ดต่อไปนี้จะใช้ภาพเคลื่อนไหวเมื่อเพิ่ม นำออก และ จัดเรียงรายการใน Lazy List ใหม่

ข้อมูลโค้ดนี้จะแสดงรายการสตริงที่มีทรานซิชันแบบเคลื่อนไหวเมื่อมีการเพิ่ม นำออก หรือจัดเรียงใหม่

@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),
            )
        }
    }
}

ประเด็นสำคัญเกี่ยวกับโค้ด

  • ListAnimatedItems แสดงรายการสตริงใน LazyColumn พร้อม การเปลี่ยนภาพเคลื่อนไหวเมื่อมีการแก้ไขรายการ
  • ฟังก์ชัน items จะกำหนดคีย์ที่ไม่ซ้ำกันให้กับแต่ละรายการในลิสต์ Compose ใช้คีย์เพื่อติดตามรายการและระบุการเปลี่ยนแปลงตำแหน่งของรายการ
  • ListItem กำหนดเลย์เอาต์ของแต่ละรายการในลิสต์ โดยจะใช้headlineContent พารามิเตอร์ซึ่งกำหนดเนื้อหาหลักของรายการ
  • ตัวแก้ไข animateItem จะใช้ภาพเคลื่อนไหวเริ่มต้นกับการเพิ่ม การนำออก และการย้ายรายการ

ข้อมูลโค้ดต่อไปนี้แสดงหน้าจอที่มีตัวควบคุมสำหรับการเพิ่ม และนำรายการออก รวมถึงการจัดเรียงรายการที่กำหนดไว้ล่วงหน้า

@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)
        }
    }
}

ประเด็นสำคัญเกี่ยวกับโค้ด

  • ListAnimatedItemsExample จะแสดงหน้าจอที่มีตัวควบคุมสำหรับ การเพิ่ม นำออก และจัดเรียงรายการ
    • onAddItem และ onRemoveItem คือนิพจน์ Lambda ที่ส่งไปยัง AddRemoveButtons เพื่อเพิ่มและนำรายการออกจากรายการ
    • resetOrder, onSortAlphabetically และ onSortByLength คือนิพจน์ Lambda ที่ส่งไปยัง OrderButtons เพื่อเปลี่ยนลำดับของ รายการในลิสต์
  • AddRemoveButtons จะแสดงปุ่ม "เพิ่ม" และ "นำออก" ซึ่งจะ เปิด/ปิดใช้ปุ่มและจัดการการคลิกปุ่ม
  • OrderButtons จะแสดงปุ่มสำหรับจัดเรียงรายการใหม่ โดยจะรับ ฟังก์ชัน Lambda สำหรับรีเซ็ตลำดับและจัดเรียงรายการตามความยาวหรือ ตามตัวอักษร
  • ListAnimatedItems เรียกใช้ Composable ListAnimatedItems โดยส่งรายการ data เพื่อแสดงรายการสตริงแบบเคลื่อนไหว data ได้รับการกำหนดไว้ ที่อื่น

ข้อมูลโค้ดนี้จะสร้าง UI ที่มีปุ่มเพิ่มรายการและลบรายการ

@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")
        }
    }
}

ประเด็นสำคัญเกี่ยวกับโค้ด

  • AddRemoveButtons จะแสดงแถวของปุ่มเพื่อดำเนินการเพิ่มและนำออกจาก รายการ
  • พารามิเตอร์ canAddItem และ canRemoveItem จะควบคุมสถานะที่เปิดใช้ของปุ่ม หาก canAddItem หรือ canRemoveItem เป็นเท็จ ระบบจะปิดใช้ปุ่มที่เกี่ยวข้อง
  • พารามิเตอร์ onAddItem และ onRemoveItem คือฟังก์ชันแลมบ์ดาที่จะทํางานเมื่อผู้ใช้คลิกปุ่มที่เกี่ยวข้อง

สุดท้ายนี้ ข้อมูลโค้ดนี้จะแสดงปุ่ม 3 ปุ่มสำหรับการจัดเรียงรายการ (รีเซ็ต ตามตัวอักษรและความยาว)

@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)
                }
            }
        }
    }
}

ประเด็นสำคัญเกี่ยวกับโค้ด

  • OrderButtons จะแสดง SingleChoiceSegmentedButtonRow เพื่อให้ผู้ใช้ เลือกวิธีการจัดเรียงในรายการหรือรีเซ็ตลำดับรายการ คอมโพเนนต์ SegmentedButtonช่วยให้คุณเลือกตัวเลือกเดียวจากรายการตัวเลือกได้
  • resetOrder, orderAlphabetically และ orderByLength คือฟังก์ชัน Lambda ที่จะทำงานเมื่อเลือกปุ่มที่เกี่ยวข้อง
  • ตัวแปรสถานะ selectedIndex จะติดตามตัวเลือกที่เลือก

ผลลัพธ์

วิดีโอนี้แสดงผลลัพธ์ของข้อมูลโค้ดก่อนหน้าเมื่อมีการจัดเรียงรายการใหม่

รูปที่ 1 รายการที่เคลื่อนไหวการเปลี่ยนรายการเมื่อมีการ เพิ่ม นำออก หรือจัดเรียงรายการ

ส่วนหัวแบบติดหนึบ

รูปแบบ "ส่วนหัวแบบติดหนึบ" มีประโยชน์เมื่อแสดงรายการข้อมูลที่จัดกลุ่ม ด้านล่างนี้คือตัวอย่าง "รายชื่อติดต่อ" ที่จัดกลุ่มตามอักษรตัวแรกของชื่อ รายชื่อติดต่อแต่ละราย

วิดีโอของโทรศัพท์ที่เลื่อนขึ้นและลงผ่านรายชื่อติดต่อ

หากต้องการสร้างส่วนหัวแบบติดหนึบด้วย LazyColumn คุณสามารถใช้ฟังก์ชันทดลอง stickyHeader() โดยระบุเนื้อหาส่วนหัวได้ดังนี้

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

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

หากต้องการสร้างรายการที่มีส่วนหัวหลายรายการ เช่น ตัวอย่าง "รายชื่อติดต่อ" ด้านบน คุณสามารถทำได้ดังนี้

// 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)
            }
        }
    }
}

การตอบสนองต่อตำแหน่งการเลื่อน

แอปจำนวนมากต้องตอบสนองและรับฟังการเปลี่ยนแปลงตำแหน่งการเลื่อนและเลย์เอาต์ของรายการ คอมโพเนนต์ Lazy รองรับกรณีการใช้งานนี้โดยการยก LazyListState ดังนี้

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

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

สำหรับกรณีการใช้งานที่เรียบง่าย โดยทั่วไปแล้ว แอปมักจะต้องการทราบข้อมูลเกี่ยวกับ รายการแรกที่มองเห็นเท่านั้น สำหรับ LazyListState นี้จะระบุพร็อพเพอร์ตี้ firstVisibleItemIndex และ firstVisibleItemScrollOffset

หากเราใช้ตัวอย่างการแสดงและซ่อนปุ่มตามว่าผู้ใช้ได้ เลื่อนผ่านรายการแรกหรือไม่

@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()
        }
    }
}

การอ่านสถานะโดยตรงในการจัดองค์ประกอบจะมีประโยชน์เมื่อคุณต้องการอัปเดต Composables อื่นๆ ของ UI แต่ก็มีสถานการณ์ที่เหตุการณ์ไม่จำเป็นต้อง ได้รับการจัดการในการจัดองค์ประกอบเดียวกันด้วย ตัวอย่างที่พบบ่อยของกรณีนี้คือการส่ง เหตุการณ์ Analytics เมื่อผู้ใช้เลื่อนผ่านจุดหนึ่ง เราสามารถใช้snapshotFlow()เพื่อจัดการเรื่องนี้ ได้อย่างมีประสิทธิภาพ

val listState = rememberLazyListState()

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

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

LazyListState ยังให้ข้อมูลเกี่ยวกับรายการทั้งหมดที่กำลังแสดงและขอบเขตของรายการบนหน้าจอผ่านพร็อพเพอร์ตี้ layoutInfo ดูข้อมูลเพิ่มเติมได้ที่คลาส LazyListLayoutInfo

การควบคุมตำแหน่งการเลื่อน

นอกจากการตอบสนองต่อตำแหน่งการเลื่อนแล้ว การที่แอปสามารถ ควบคุมตำแหน่งการเลื่อนได้ก็เป็นประโยชน์เช่นกัน LazyListState รองรับฟังก์ชันนี้ผ่านฟังก์ชัน scrollToItem() ซึ่งจะสแนปตำแหน่งการเลื่อน "ทันที" และ animateScrollToItem() ซึ่งจะเลื่อนโดยใช้ภาพเคลื่อนไหว (หรือที่เรียกว่าการเลื่อนอย่างราบรื่น)

@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)
            }
        }
    )
}

ชุดข้อมูลขนาดใหญ่ (การแบ่งหน้า)

ไลบรารีเพจจิ้งช่วยให้แอป รองรับรายการจำนวนมากได้ โดยจะโหลดและแสดงรายการเป็นกลุ่มเล็กๆ ตามความจำเป็น Paging 3.0 ขึ้นไปรองรับ Compose ผ่านandroidx.paging:paging-compose

หากต้องการแสดงรายการเนื้อหาที่แบ่งหน้า เราสามารถใช้ฟังก์ชันส่วนขยาย collectAsLazyPagingItems() จากนั้นส่งLazyPagingItems ที่ส่งคืนไปยัง items() ใน LazyColumn คุณสามารถ แสดงตัวยึดตำแหน่งขณะที่ระบบโหลดข้อมูลได้โดยการตรวจสอบว่า item เป็น null หรือไม่ ซึ่งคล้ายกับการรองรับการแบ่งหน้าในมุมมอง

@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

มีเคล็ดลับบางอย่างที่คุณควรพิจารณาเพื่อให้เลย์เอาต์แบบ Lazy ทำงานได้ตามที่ต้องการ

หลีกเลี่ยงการใช้รายการที่มีขนาด 0 พิกเซล

กรณีนี้อาจเกิดขึ้นในสถานการณ์ที่คุณคาดว่าจะเรียกข้อมูลบางอย่าง เช่น รูปภาพ แบบไม่พร้อมกัน เพื่อเติมรายการของลิสต์ในภายหลัง ซึ่งจะทำให้เลย์เอาต์แบบ Lazy สร้างรายการทั้งหมดในการวัดครั้งแรก เนื่องจากรายการมีความสูง 0 พิกเซลและสามารถใส่รายการทั้งหมดในวิวพอร์ตได้ เมื่อโหลดรายการและขยายความสูงแล้ว เลย์เอาต์แบบ Lazy จะทิ้งรายการอื่นๆ ทั้งหมดที่สร้างขึ้นโดยไม่จำเป็น ในครั้งแรกเนื่องจากรายการเหล่านั้นไม่พอดีกับวิวพอร์ต หากต้องการหลีกเลี่ยงปัญหานี้ คุณควรตั้งค่าการปรับขนาดเริ่มต้นให้กับสินค้า เพื่อให้เลย์เอาต์แบบ Lazy สามารถคำนวณได้อย่างถูกต้องว่ามีสินค้ากี่รายการที่พอดีกับวิวพอร์ต

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

เมื่อทราบขนาดโดยประมาณของรายการหลังจากโหลดข้อมูลแบบอะซิงโครนัสแล้ว แนวทางปฏิบัติที่ดีคือการตรวจสอบว่าการปรับขนาดของรายการยังคงเหมือนเดิมทั้งก่อนและหลังการโหลด เช่น โดยการเพิ่มตัวยึดตำแหน่ง ซึ่งจะช่วยรักษําแหน่งการเลื่อนที่ถูกต้อง

หลีกเลี่ยงการซ้อนคอมโพเนนต์ที่เลื่อนได้ในทิศทางเดียวกัน

ซึ่งจะมีผลเฉพาะในกรณีที่ซ้อนองค์ประกอบย่อยที่เลื่อนได้โดยไม่มีขนาดที่กำหนดไว้ล่วงหน้าภายในองค์ประกอบหลักที่เลื่อนได้ในทิศทางเดียวกัน เช่น การพยายาม ซ้อนองค์ประกอบย่อย LazyColumn โดยไม่มีความสูงคงที่ภายในองค์ประกอบหลักที่เลื่อนได้ในแนวตั้ง Column

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

แต่คุณสามารถได้ผลลัพธ์เดียวกันโดยการห่อ Composable ทั้งหมด ไว้ใน LazyColumn หลักรายการเดียว และใช้ DSL ของ Composable นั้นเพื่อส่งเนื้อหาประเภทต่างๆ ซึ่งช่วยให้คุณส่งรายการเดียวและรายการหลายรายการได้ ทั้งหมดในที่เดียว

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

โปรดทราบว่าระบบอนุญาตให้ใช้กรณีที่คุณซ้อนเลย์เอาต์ทิศทางต่างๆ เช่น เลย์เอาต์หลักที่เลื่อนได้ Row และเลย์เอาต์ย่อย LazyColumn

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

รวมถึงกรณีที่คุณยังคงใช้เลย์เอาต์ทิศทางเดียวกัน แต่ตั้งค่า ขนาดคงที่ให้กับองค์ประกอบย่อยที่ซ้อนกันด้วย

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

ระวังการใส่องค์ประกอบหลายรายการในรายการเดียว

ในตัวอย่างนี้ Lambda ของรายการที่ 2 จะปล่อยรายการ 2 รายการในบล็อกเดียว ดังนี้

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

เลย์เอาต์แบบเลซี่จะจัดการเรื่องนี้ตามที่คาดไว้ โดยจะวางองค์ประกอบทีละรายการ ต่อๆ กันราวกับว่าเป็นรายการที่แตกต่างกัน อย่างไรก็ตาม การทำเช่นนี้มีปัญหาอยู่ 2-3 อย่าง

เมื่อมีการส่งองค์ประกอบหลายรายการเป็นส่วนหนึ่งของรายการเดียว ระบบจะถือว่าเป็น เอนทิตีเดียว ซึ่งหมายความว่าคุณจะเขียนองค์ประกอบแต่ละรายการแยกกันไม่ได้อีกต่อไป หากองค์ประกอบหนึ่งปรากฏบนหน้าจอ องค์ประกอบทั้งหมดที่สอดคล้องกับรายการจะต้องได้รับการจัดวางและวัดขนาด ซึ่งอาจส่งผลเสียต่อประสิทธิภาพหากใช้มากเกินไป ในกรณีที่แย่ที่สุดคือการใส่องค์ประกอบทั้งหมดในรายการเดียว ซึ่งจะทำให้การใช้เลย์เอาต์แบบ Lazy ไม่ได้ผลโดยสิ้นเชิง นอกเหนือจากปัญหาด้านประสิทธิภาพที่อาจเกิดขึ้นแล้ว การใส่องค์ประกอบหลายรายการในสินค้าชิ้นเดียวจะรบกวน scrollToItem() & animateScrollToItem() ด้วย

อย่างไรก็ตาม มีกรณีการใช้งานที่ถูกต้องสำหรับการใส่องค์ประกอบหลายรายการในรายการเดียว เช่น การมีตัวคั่นภายในรายการ คุณไม่ต้องการให้ตัวคั่นเปลี่ยนดัชนีการเลื่อน เนื่องจากไม่ควรพิจารณาว่าเป็นองค์ประกอบอิสระ นอกจากนี้ ประสิทธิภาพจะไม่ได้รับผลกระทบเนื่องจากตัวคั่นมีขนาดเล็ก โดยทั่วไปแล้ว ตัวคั่นควร มองเห็นได้เมื่อรายการก่อนหน้ามองเห็นได้ เพื่อให้ตัวคั่นเป็น ส่วนหนึ่งของรายการก่อนหน้า

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

พิจารณาใช้การจัดเรียงที่กำหนดเอง

โดยปกติแล้ว Lazy List จะมีหลายรายการและมีขนาดใหญ่กว่าคอนเทนเนอร์เลื่อน อย่างไรก็ตาม เมื่อรายการมีข้อมูลเพียงไม่กี่รายการ การออกแบบอาจมีข้อกำหนดที่เฉพาะเจาะจงมากขึ้นเกี่ยวกับวิธีจัดวางรายการเหล่านี้ในวิวพอร์ต

คุณสามารถใช้แนวตั้งที่กำหนดเอง Arrangement และส่งไปยัง LazyColumn เพื่อให้บรรลุเป้าหมายนี้ ในตัวอย่างต่อไปนี้ TopWithFooter ออบเจ็กต์ต้องใช้เมธอด arrange เท่านั้น ประการแรกคือจะจัดวาง รายการต่อกัน ประการที่สอง หากความสูงที่ใช้ทั้งหมดต่ำกว่า ความสูงของวิวพอร์ต ระบบจะวางตำแหน่งส่วนท้ายไว้ที่ด้านล่าง

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

ตั้งแต่ Compose 1.2 เป็นต้นไป หากต้องการเพิ่มประสิทธิภาพของ Lazy layout ให้สูงสุด ให้ลองเพิ่ม contentType ลงในรายการหรือตารางกริด ซึ่งจะช่วยให้คุณระบุประเภทเนื้อหาสำหรับแต่ละรายการของเลย์เอาต์ได้ในกรณีที่คุณกำลังสร้างรายการหรือตารางกริดที่มีรายการหลายประเภทแตกต่างกัน

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

เมื่อคุณระบุ contentType Compose จะใช้การจัดองค์ประกอบซ้ำได้เฉพาะ ระหว่างรายการประเภทเดียวกันเท่านั้น การนำกลับมาใช้ใหม่จะมีประสิทธิภาพมากกว่าเมื่อคุณ สร้างรายการที่มีโครงสร้างคล้ายกัน การระบุประเภทเนื้อหาจะช่วยให้ Compose ไม่พยายามสร้างรายการประเภท A บนรายการประเภท B ที่แตกต่างกันโดยสิ้นเชิง ซึ่งจะช่วยเพิ่มประโยชน์สูงสุดจากการคอมโพส การนำกลับมาใช้ใหม่ และประสิทธิภาพของเลย์เอาต์แบบ Lazy

การวัดประสิทธิภาพ

คุณจะวัดประสิทธิภาพของเลย์เอาต์แบบ Lazy ได้อย่างน่าเชื่อถือเมื่อเรียกใช้ใน โหมดรีลีสและเปิดใช้การเพิ่มประสิทธิภาพ R8 เท่านั้น ในบิลด์สำหรับดีบัก การเลื่อนเลย์เอาต์แบบ Lazy อาจดูช้าลง ดูข้อมูลเพิ่มเติมได้ที่ประสิทธิภาพการเขียน

แหล่งข้อมูลเพิ่มเติม