แอปจำนวนมากจำเป็นต้องแสดงคอลเล็กชันของรายการต่างๆ เอกสารนี้อธิบายวิธีดำเนินการนี้อย่างมีประสิทธิภาพใน 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") } // ... }
ตารางกริดแบบสลับที่โหลดอย่างช้าๆ
LazyVerticalStaggeredGrid
และ
LazyHorizontalStaggeredGrid
เป็นองค์ประกอบที่ช่วยให้คุณสร้างตารางกริดแบบเหลื่อมของรายการที่โหลดแบบ Lazy Loading ได้
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() )
หากต้องการตั้งค่าจำนวนคอลัมน์คงที่ ให้ใช้
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 ไปยัง 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 รองรับประเภทต่างๆ เช่น Primitive
Enums หรือ 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เพื่อให้ผู้ใช้เลือกวิธีการจัดเรียงในรายการหรือรีเซ็ตลำดับรายการได้ คอมโพเนนต์SegmentedButtonช่วยให้คุณเลือกตัวเลือกเดียวจากรายการตัวเลือกได้resetOrder,orderAlphabeticallyและorderByLengthคือฟังก์ชัน Lambda ที่จะทำงานเมื่อมีการเลือกปุ่มที่เกี่ยวข้อง- ตัวแปรสถานะ
selectedIndexจะติดตามตัวเลือกที่เลือก
ผลลัพธ์
วิดีโอนี้แสดงผลลัพธ์ของข้อมูลโค้ดก่อนหน้าเมื่อมีการจัดเรียงรายการใหม่
ส่วนหัวแบบติดหนึบ
รูปแบบ "ส่วนหัวแบบติดหนึบ" มีประโยชน์เมื่อแสดงรายการข้อมูลที่จัดกลุ่ม ด้านล่างนี้คือตัวอย่าง "รายชื่อติดต่อ" ที่จัดกลุ่มตามอักษรตัวแรกของชื่อ รายชื่อติดต่อแต่ละราย

หากต้องการสร้างส่วนหัวแบบติดหนึบด้วย 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 พิกเซลและสามารถใส่รายการทั้งหมดใน Viewport ได้ เมื่อโหลดรายการและขยายความสูงแล้ว เลย์เอาต์แบบ 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 ไม่พยายามเขียนรายการประเภท A บนรายการประเภท B ที่แตกต่างกันโดยสิ้นเชิง ซึ่งจะช่วยเพิ่มประโยชน์สูงสุดจากการคอมโพส
การนำกลับมาใช้ใหม่ และประสิทธิภาพของเลย์เอาต์แบบ Lazy
การวัดประสิทธิภาพ
คุณจะวัดประสิทธิภาพของเลย์เอาต์แบบ Lazy ได้อย่างน่าเชื่อถือเมื่อเรียกใช้ใน โหมดรีลีสและเปิดใช้การเพิ่มประสิทธิภาพ R8 เท่านั้น ในบิลด์การแก้ไขข้อบกพร่อง การเลื่อนเลย์เอาต์แบบ Lazy อาจดูช้าลง ดูข้อมูลเพิ่มเติมได้ที่ประสิทธิภาพการเขียน
แหล่งข้อมูลเพิ่มเติม
- สร้างรายการที่เลื่อนได้แบบจำกัด
- สร้างตารางกริดที่เลื่อนได้
- แสดงรายการที่เลื่อนได้แบบซ้อนกันในรายการ
- กรองรายการขณะพิมพ์
- โหลดข้อมูลแบบ Lazy Loading ด้วยรายการและเพจจิ้ง
- สร้างรายการโดยใช้ไอเทมหลายประเภท
- วิดีโอ: รายการใน Compose
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- ย้ายข้อมูล
RecyclerViewไปยัง Lazy List - บันทึกสถานะ UI ใน Compose
- Kotlin สำหรับ Jetpack Compose