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

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

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

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

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

รายการแบบ Lazy Loading

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

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

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

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

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 Loading

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

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

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

บางครั้ง คุณจะต้องเพิ่มระยะห่างจากขอบรอบขอบของเนื้อหา คนขี้เกียจ จะช่วยให้คุณสามารถส่ง 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 นี้ใช้งานง่าย คุณเพียงแค่ต้องตั้งค่าตัวแก้ไข animateItemPlacement ให้กับเนื้อหาสินค้า ดังนี้

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

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

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

ไลบรารีการแบ่งหน้าช่วยให้แอปรองรับรายการรายการขนาดใหญ่ โหลดและแสดงรายการทีละส่วนตามที่จำเป็น หน้า 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 Loading

ต่อไปนี้เป็นเคล็ดลับบางส่วนที่คุณควรคำนึงถึงเพื่อให้เลย์เอาต์แบบ 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 หลัก 1 ราย และใช้ DSL ของตนเองเพื่อส่งต่อ เนื้อหา ซึ่งช่วยให้คุณส่งออกรายการเดี่ยวและรายการหลายรายการในรายการได้ในที่เดียว

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 lambda จะแสดง 2 รายการใน 1 บล็อก

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

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

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

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

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

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

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

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

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

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

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

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