แอปจำนวนมากจำเป็นต้องแสดงคอลเล็กชันของรายการ เอกสารนี้อธิบายวิธีดำเนินการนี้อย่างมีประสิทธิภาพใน Jetpack Compose
หากทราบว่า Use Case ของคุณไม่จำเป็นต้องมีการเลื่อน คุณอาจต้องการ
ใช้ 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
ปล่อย 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
Composable
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") } // ... }
ตารางกริดแบบเหลื่อมที่โหลดเมื่อจำเป็น
LazyVerticalStaggeredGrid
และ
LazyHorizontalStaggeredGrid
เป็น Composable ที่ช่วยให้คุณสร้างตารางกริดแบบสลับรายการที่โหลดแบบ Lazy ของรายการต่างๆ ได้
เลย์เอาต์กริดแบบสลับแนวตั้งที่โหลดแบบ Lazy จะแสดงรายการในคอนเทนเนอร์ที่เลื่อนได้ในแนวตั้งซึ่งครอบคลุมหลายคอลัมน์และอนุญาตให้แต่ละรายการมีความสูงแตกต่างกันได้ 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() )
หากต้องการตั้งค่าจำนวนคอลัมน์คงที่ คุณสามารถใช้
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
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
ไปยัง LazyColumn
ของ contentPadding
ได้ ดูคำแนะนำเกี่ยวกับการแสดงผลแบบขอบจรดขอบ
การเว้นวรรคเนื้อหา
หากต้องการเพิ่มระยะห่างระหว่างรายการ คุณสามารถใช้
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
รองรับประเภทต่างๆ เช่น Primitive, Enum หรือ 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
เรียกใช้ ComposableListAnimatedItems
โดยส่งรายการ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
คือ Lambda ที่จะทํางานเมื่อผู้ใช้คลิกปุ่มที่เกี่ยวข้อง
สุดท้ายนี้ ข้อมูลโค้ดนี้จะแสดงปุ่ม 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
เพื่อให้ผู้ใช้ เลือกวิธีการจัดเรียงในรายการหรือรีเซ็ตลำดับรายการ คอมโพเนนต์ ASegmentedButton
ช่วยให้คุณเลือกตัวเลือกเดียวจาก รายการตัวเลือกได้resetOrder
,orderAlphabetically
และorderByLength
คือฟังก์ชัน Lambda ที่จะทำงานเมื่อมีการเลือกปุ่มที่เกี่ยวข้อง- ตัวแปรสถานะ
selectedIndex
จะติดตามตัวเลือกที่เลือก
ผลลัพธ์
วิดีโอนี้แสดงผลลัพธ์ของตัวอย่างข้อมูลก่อนหน้าเมื่อมีการจัดเรียงรายการใหม่
ส่วนหัวแบบติดหนึบ (ทดลอง)
รูปแบบ "ส่วนหัวแบบติดหนึบ" มีประโยชน์เมื่อแสดงรายการข้อมูลที่จัดกลุ่ม ด้านล่างนี้คือตัวอย่าง "รายชื่อติดต่อ" ที่จัดกลุ่มตามอักษรตัวแรกของชื่อ รายชื่อติดต่อแต่ละราย
หากต้องการสร้างส่วนหัวแบบติดหนึบด้วย LazyColumn
คุณสามารถใช้ฟังก์ชันทดลอง
stickyHeader()
โดยระบุเนื้อหาส่วนหัวได้ดังนี้
@OptIn(ExperimentalFoundationApi::class) @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] } @OptIn(ExperimentalFoundationApi::class) @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 จะมีหลายรายการและมีขนาดใหญ่กว่าคอนเทนเนอร์เลื่อน อย่างไรก็ตาม เมื่อรายการมีข้อมูลเพียงไม่กี่รายการ การออกแบบอาจมีข้อกำหนดที่เฉพาะเจาะจงมากขึ้นเกี่ยวกับวิธีจัดวางรายการเหล่านี้ใน Viewport
คุณสามารถใช้ประเภทธุรกิจที่กำหนดเอง
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 ไม่พยายามเขียนรายการประเภท ก. ทับรายการประเภท ข. ที่แตกต่างกันโดยสิ้นเชิง
ซึ่งจะช่วยเพิ่มประโยชน์สูงสุดจากการ
นำองค์ประกอบกลับมาใช้ซ้ำและประสิทธิภาพของเลย์เอาต์แบบ Lazy
การวัดประสิทธิภาพ
คุณจะวัดประสิทธิภาพของเลย์เอาต์แบบ Lazy ได้อย่างน่าเชื่อถือเมื่อเรียกใช้ใน โหมดรีลีสและเปิดใช้การเพิ่มประสิทธิภาพ R8 เท่านั้น ในบิลด์การแก้ไขข้อบกพร่อง การเลื่อนเลย์เอาต์แบบ Lazy อาจดูช้าลง ดูข้อมูลเพิ่มเติมได้ที่ประสิทธิภาพการเขียน
แหล่งข้อมูลเพิ่มเติม
- สร้างรายการที่เลื่อนได้แบบจำกัด
- สร้างตารางกริดที่เลื่อนได้
- แสดงรายการที่เลื่อนได้แบบซ้อนกันในรายการ
- กรองรายการขณะพิมพ์
- โหลดข้อมูลแบบเลื่อนด้วยรายการและการแบ่งหน้า
- สร้างรายการโดยใช้ไอเทมหลายประเภท
- วิดีโอ: รายการในฟีเจอร์เขียน
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- ย้ายข้อมูล
RecyclerView
ไปยัง Lazy list - บันทึกสถานะ UI ใน Compose
- Kotlin สำหรับ Jetpack Compose