รายการและตารางกริด

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

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

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

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

รายการแบบ Lazy

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

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

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

คอมโพเนนต์แบบ Lazy แตกต่างจากเลย์เอาต์ส่วนใหญ่ใน Compose คอมโพเนนต์แบบเลื่อนออกจะแสดงบล็อก LazyListScope.() แทนที่จะรับพารามิเตอร์บล็อกเนื้อหา @Composable ซึ่งช่วยให้แอปแสดงคอมโพสิเบิลได้โดยตรง บล็อก 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 จะแสดงรายการในคอนเทนเนอร์ที่เลื่อนในแนวตั้ง ซึ่งครอบคลุมหลายคอลัมน์ ส่วนตารางกริดแนวนอนแบบ Lazy จะมีลักษณะการทำงานแบบเดียวกันในแกนแนวนอน

ตารางกริดมีความสามารถของ 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

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

ข้อมูลโค้ดต่อไปนี้เป็นตัวอย่างพื้นฐานของการใช้ 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 ในเครื่องมือเขียน
รูปที่ 2 ตัวอย่างตารางกริดแนวตั้งแบบ Lazy ที่เยื้องกันโดยมีคอลัมน์คงที่

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

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

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

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

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

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

หากต้องการเพิ่มระยะห่างระหว่างรายการต่างๆ ให้ใช้ 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 จัดการการเรียงลําดับใหม่ได้อย่างถูกต้อง ตัวอย่างเช่น หากรายการมีสถานะที่จดจำไว้ การตั้งค่าคีย์จะช่วยให้คอมโพสิชันย้ายสถานะนี้ไปพร้อมกับรายการได้เมื่อตำแหน่งของรายการเปลี่ยนแปลง

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

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

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

Bundle ต้องรองรับคีย์ดังกล่าวเพื่อให้ระบบกู้คืน rememberSaveable ภายในคอมโพสิเบิลของรายการได้เมื่อสร้างกิจกรรมขึ้นมาใหม่ หรือแม้แต่เมื่อคุณเลื่อนออกจากรายการนี้และเลื่อนกลับ

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

ภาพเคลื่อนไหวของไอเทม

หากเคยใช้วิดเจ็ต RecyclerView คุณจะทราบว่าวิดเจ็ตดังกล่าวจะแสดงภาพการเปลี่ยนแปลงของรายการโดยอัตโนมัติ เลย์เอาต์แบบ Lazy มีฟังก์ชันการทำงานเดียวกันสำหรับการเรียงลำดับรายการใหม่ 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)
            )
        ) {
            // ...
        }
    }
}

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

ส่วนหัวแบบติดหนึบ (ทดลอง)

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

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

หากต้องการใช้ส่วนหัวแบบติดหนึบด้วย 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) {
        // ...
    }
}

สําหรับ Use Case ง่ายๆ แอปมักจําเป็นต้องทราบข้อมูลเกี่ยวกับรายการแรกที่มองเห็นเท่านั้น ด้วยเหตุนี้ 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()
        }
    }
}

การอ่านสถานะในคอมโพสิชันโดยตรงจะมีประโยชน์เมื่อคุณต้องการอัปเดตคอมโพสิชัน 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 ขึ้นไปรองรับการเขียนผ่านไลบรารี 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 {
        // ...
    }
}

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

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

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

ในตัวอย่างนี้ แลมดารายการที่ 2 จะแสดงผล 2 รายการในบล็อกเดียว

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

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

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

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

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

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

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

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

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 เป็นต้นไป โปรดพิจารณาเพิ่ม contentType ลงในรายการหรือตารางกริดเพื่อให้ได้ประสิทธิภาพสูงสุดจากเลย์เอาต์แบบ Lazy ซึ่งช่วยให้คุณระบุประเภทเนื้อหาสำหรับรายการแต่ละรายการของเลย์เอาต์ได้ในกรณีที่คุณกำลังเขียนรายการหรือตารางกริดที่มีรายการหลายประเภท

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

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

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

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