LazyColumn — คอมโพเนนต์ Jetpack Compose สำหรับแสดงรายการที่เลื่อนได้พร้อมการโหลดแบบขี้เกียจ แตกต่างจาก RecyclerView ตรงที่ LazyColumn ไม่สร้าง ViewHolders และไม่ใช้ XML — องค์ประกอบทั้งหมดถูกอธิบายแบบประกาศผ่านฟังก์ชัน composable เราแสดงให้เห็นว่า LazyColumn คืออะไร LazyListState และ items API ทำงานอย่างไร และวิธีทำให้ได้ประสิทธิภาพ 60 FPS ที่ IT Sectr เราใช้ LazyColumn ในโปรเจกต์ Compose ใหม่ทั้งหมด สำหรับการเปรียบเทียบกับแนวทางคลาสสิก อ่านบทความเกี่ยวกับ RecyclerView
ประเด็นสำคัญ
items(), itemsIndexed(), items() พร้อมคีย์สำหรับการผูกข้อมูลLazyColumn คือฟังก์ชัน composable จากไลบรารี Foundation Compose ที่แสดงรายการแนวตั้งขององค์ประกอบแบบโหลดขี้เกียจ: เฉพาะองค์ประกอบที่มองเห็นบนหน้าจอหรือใกล้เคียง (โซน prefetch) เท่านั้นที่ถูกสร้างและคอมโพส LazyColumn ปรากฏใน Compose 1.0 (2021) พร้อมกับ LazyRow (รายการแนวนอน) และ LazyVerticalGrid (กริด)
การโหลดขี้เกียจ ทำงานผ่านกลไก SubcomposeLayout: LazyColumn วัดพื้นที่ว่าง ขอช่วงที่มองเห็นจาก LayoutInfo และคอมโพสเฉพาะองค์ประกอบในช่วงนั้น องค์ประกอบที่เลื่อนออกจากหน้าจอจะออกจากคอมโพซิชัน (ยกเว้นบัฟเฟอร์ prefetch) ตามข้อมูลของ Google (Android Performance, 2026) LazyColumn รักษา 60 FPS เมื่อเลื่อนผ่านรายการ 10000+ องค์ประกอบบนอุปกรณ์ระดับกลาง
Column พร้อมการเลื่อนแนวตั้ง — คือการจัดเรียงปกติขององค์ประกอบทั้งหมดเรียงกันโดยใช้มอดิไฟเออร์ verticalScroll Column ไม่ขี้เกียจ: องค์ประกอบย่อยทั้งหมดถูกคอมโพสทันที แม้ว่าจะไม่มองเห็นก็ตาม สำหรับรายการที่มี 200+ องค์ประกอบ Column ทำให้เกิดความล่าช้าอย่างมากเมื่อเริ่มต้น LazyColumn คอมโพสเฉพาะองค์ประกอบที่มองเห็น + บัฟเฟอร์ (ค่าเริ่มต้น 1 หน้าจอไปข้างหน้า/ข้างหลัง) ทำให้จำเป็นสำหรับรายการยาว
items API คือชุดของฟังก์ชันส่วนขยายสำหรับ LazyColumn ที่รับรายการข้อมูลและตัวอธิบาย DSL สำหรับแต่ละองค์ประกอบ ฟังก์ชันหลักได้แก่: items(count, key, itemContent) สำหรับจำนวนคงที่, items(list, key, itemContent) สำหรับรายการ, itemsIndexed(list, key, itemContent) พร้อมดัชนี พารามิเตอร์ key จำเป็นสำหรับประสิทธิภาพ — มันให้ตัวระบุองค์ประกอบที่เสถียรแก่ Compose
// LazyColumn พื้นฐานกับ itemContent
@Composable
fun ArticleList(articles: List<Article>) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// Header — ไม่ขี้เกียจ, อยู่ที่จุดเริ่มต้นเสมอ
item {
Text("บทความล่าสุด", style = MaterialTheme.typography.headlineMedium)
}
// รายการบทความพร้อมคีย์ตาม id
items(articles, key = { it.id }) { article ->
ArticleCard(
title = article.title,
summary = article.summary,
onClick = { onArticleClick(article.id) }
)
}
// Footer พร้อมตัวบ่งชี้การโหลด
item {
CircularProgressIndicator(
modifier = Modifier.fillMaxWidth().padding(16.dp)
)
}
}
}
// องค์ประกอบรายการที่กำหนดเอง
@Composable
fun ArticleCard(title: String, summary: String, onClick: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
) {
Column(modifier = Modifier.padding(12.dp)) {
Text(text = title, style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(4.dp))
Text(text = summary, style = MaterialTheme.typography.bodyMedium)
}
}
}
item {} เทียบกับ items(): ใช้ item {} สำหรับองค์ประกอบเดี่ยวที่อยู่ในรายการเสมอ (หัวเรื่อง, ส่วนท้าย, ตัวคั่น) ใช้ items() สำหรับรายการข้อมูลแบบไดนามิก items() รับ Iterable หรือ Int และบล็อก DSL ที่ถูกเรียกสำหรับแต่ละองค์ประกอบ รวม item และ items ใน LazyColumn เดียวกัน: ลำดับการเรียกกำหนดลำดับการแสดงผล
LazyListState คือออบเจกต์ที่เก็บสถานะของรายการ: ตำแหน่งการเลื่อนปัจจุบัน (firstVisibleItemIndex, firstVisibleItemScrollOffset), องค์ประกอบที่มองเห็น (layoutInfo), และวิธีการเลื่อนแบบโปรแกรม (scrollToItem, animateScrollToItem) LazyListState ถูกสร้างผ่าน rememberLazyListState() และส่งต่อไปยัง LazyColumn ผ่านพารามิเตอร์ state
// LazyColumn พร้อมรักษาตำแหน่งการเลื่อน
@Composable
fun ScrollingList(items: List<Item>) {
val listState = rememberLazyListState()
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(state = listState) {
items(items, key = { it.id }) { item ->
ListItemView(item = item)
}
}
// ปุ่ม "ขึ้น" ปรากฏหลังจากองค์ประกอบที่ 10
val showButton by remember {
derivedStateOf { listState.firstVisibleItemIndex > 10 }
}
AnimatedVisibility(visible = showButton) {
FloatingActionButton(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
onClick = {
coroutineScope.launch {
listState.animateScrollToItem(0)
}
}
) {
Icon(Icons.Default.KeyboardArrowUp, "กลับไปด้านบน")
}
}
}
}
// การรับข้อมูลองค์ประกอบที่มองเห็น
@Composable
fun ListDebugInfo(listState: LazyListState) {
val visibleItems = listState.layoutInfo.visibleItemsInfo
val totalItems = listState.layoutInfo.totalItemsCount
val firstItem = visibleItems.firstOrNull()?.index
val lastItem = visibleItems.lastOrNull()?.index
Text("กำลังแสดง $firstItem–$lastItem จาก $totalItems")
}
การเลื่อนแบบโปรแกรม ดำเนินการผ่านฟังก์ชัน suspend ของ listState: scrollToItem(index, scrollOffset) — เคลื่อนที่ทันที (ไม่มีแอนิเมชัน), animateScrollToItem(index) — พร้อมแอนิเมชัน สำหรับการเลื่อนจาก coroutine ให้ใช้ LaunchedEffect หรือ coroutineScope.launch สำคัญ: ห้ามเรียกฟังก์ชัน suspend จากคอมโพซิชัน (ภายใน @Composable) — ให้ใช้แลมบ์ดา (onClick, LaunchedEffect)
LazyColumn และ RecyclerView แก้ปัญหาเดียวกัน — การแสดงรายการยาวอย่างมีประสิทธิภาพ ความแตกต่างอยู่ในสถาปัตยกรรม: RecyclerView ใช้ ViewHolder และ Adapter (Android View แบบคลาสสิก), LazyColumn ใช้ฟังก์ชัน composable แบบประกาศโดยไม่ใช้ XML หรือ ViewHolder การเลือกขึ้นอยู่กับสแต็กเทคโนโลยีของโปรเจกต์: Compose เทียบกับ UI แบบ View
| พารามิเตอร์ | LazyColumn (Compose) | RecyclerView (View) |
|---|---|---|
| เลย์เอาต์ | ฟังก์ชัน Composable (Kotlin DSL) | ไฟล์ XML + ViewBinding |
| อะแดปเตอร์ | items() / itemsIndexed() DSL | RecyclerView.Adapter + ViewHolder |
| การเรียง/การกรอง | Snapshot state + derivedStateOf | DiffUtil + AsyncListDiffer |
| แอนิเมชัน | AnimatedVisibility + Modifier.animateItem | DefaultItemAnimator |
| ประเภทองค์ประกอบที่แตกต่าง | item {}/items() + when ตามประเภท | getItemViewType + หลาย ViewHolders |
| เลื่อนไปยังตำแหน่ง | listState.animateScrollToItem() | layoutManager.scrollToPosition() |
| Prefetch | ในกระบวนการ (บัฟเฟอร์ SubcomposeLayout) | RecycledViewPool + GapWorker |
| เครือข่าย/การแบ่งหน้า | collectAsLazyPagingItems() + Paging 3 | PagingDataAdapter + Paging 3 |
คำแนะนำของ Google (Android Developers, 2026): สำหรับโปรเจกต์ใหม่บน Jetpack Compose ให้ใช้ LazyColumn สำหรับโปรเจกต์แบบ View ให้ใช้ RecyclerView LazyColumn ต้องใช้ Compose ซึ่งเพิ่มประมาณ 3–5 MB ใน APK — พิจารณาเรื่องนี้เมื่อรองรับอุปกรณ์เก่า หากโปรเจกต์ใช้ระบบ View อยู่แล้ว LazyColumn ไม่สามารถอยู่ร่วมในหน้าจอเดียวกับเลย์เอาต์ XML โดยไม่มี AndroidView — ในกรณีเช่นนี้ ควรใช้ RecyclerView ต่อไปจะง่ายกว่า
LazyVerticalGrid คือเวอร์ชันของ LazyColumn สำหรับกริดที่มีจำนวนคอลัมน์คงที่ LazyHorizontalGrid คือกริดแนวนอนที่มีแถวคงที่ ทั้งสองคอมโพเนนต์ใช้หลักการโหลดขี้เกียจเดียวกันและ items API เดียวกับ LazyColumn พารามิเตอร์ columns กำหนดจำนวนคอลัมน์ผ่าน GridCells.Fixed(N) หรือ GridCells.Adaptive(minSize)
// กริดพร้อมคอลัมน์แบบปรับได้
@Composable
fun PhotoGrid(photos: List<Photo>) {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 128.dp),
contentPadding = PaddingValues(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(photos, key = { it.id }) { photo ->
AsyncImage(
model = photo.url,
contentDescription = photo.title,
modifier = Modifier.aspectRatio(1f)
)
}
}
}
// รายการแนวนอน (LazyRow)
@Composable
fun CategoryCarousel(categories: List<Category>) {
LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
items(categories, key = { it.id }) { category ->
FilterChip(
selected = category.isSelected,
onClick = { onCategoryClick(category.id) },
label = { Text(category.name) }
)
}
}
}
GridCells.Adaptive คำนวณจำนวนคอลัมน์โดยอัตโนมัติตามขนาดเซลล์ขั้นต่ำ ตัวอย่างเช่น GridCells.Adaptive(128.dp) บนหน้าจอกว้าง 360 dp วาง 2 คอลัมน์ บนแท็บเล็ต 600 dp — 4 คอลัมน์ GridCells.Fixed(2) แสดง 2 คอลัมน์เสมอ Adaptive เหมาะกว่าสำหรับการปรับหลายหน้าจอ — มันปรับตามความกว้างของหน้าจอโดยอัตโนมัติโดยไม่ต้องมี media query
ประสิทธิภาพ ของ LazyColumn ขึ้นอยู่กับสามปัจจัย: ความเสถียรของคีย์ (key), ประเภทเนื้อหา (contentType), และบัฟเฟอร์ prefetch (beyondBounds) โดยค่าเริ่มต้น LazyColumn จะบัฟเฟอร์ 1 หน้าจอไปข้างหน้าและ 1 หน้าจอข้างหลัง Google (Android Developers, 2026) แนะนำให้กำหนดค่าพารามิเตอร์เหล่านี้สำหรับรายการที่มีประเภทองค์ประกอบแตกต่างกันหรือคอมโพซิชันที่ซับซ้อน
| พารามิเตอร์ | คำอธิบาย | คำแนะนำ |
|---|---|---|
| key | ตัวระบุองค์ประกอบที่เสถียร | ระบุ key = { it.id } เสมอ หากไม่มีคีย์ Compose จะใช้ดัชนี และการเรียงลำดับองค์ประกอบใหม่จะทำให้แอนิเมชันเสีย |
| contentType | ประเภทเนื้อหาสำหรับการแยกพูล | ระบุสำหรับรายการที่มีประเภทองค์ประกอบแตกต่างกัน (ข้อความ + รูปภาพ + โฆษณา) Compose จะนำคอมโพซิชันกลับมาใช้ใหม่เฉพาะภายใน contentType เดียวกัน |
| beyondBounds | จำนวนหน้าจอ prefetch | เพิ่มเป็น 2–3 สำหรับรายการที่มีรูปภาพหนัก ค่าเริ่มต้นคือ 1 หน้าจอ |
| Modifier.animateItem | แอนิเมชันการเปลี่ยนตำแหน่ง | ใช้สำหรับการเรียงลำดับและการกรองแบบมีแอนิเมชัน (Compose 1.7+) |
// LazyColumn ที่ปรับแต่งด้วย contentType และ prefetch
@Composable
fun OptimizedFeed(items: List<FeedItem>) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
beyondBoundsPageCount = 2 // prefetch 2 หน้าจอ
) {
items(
items = items,
key = { it.id },
contentType = { it.type } // การแบ่งตามประเภท
) { item ->
when (item) {
is FeedItem.Post -> PostView(item)
is FeedItem.Ad -> AdView(item)
is FeedItem.Suggested -> SuggestedView(item)
}
}
}
}
// การวัดประสิทธิภาพผ่าน listState
@Composable
fun ScrollPerformance(listState: LazyListState) {
val scrollInfo = listState.layoutInfo
val visibleCount = scrollInfo.visibleItemsInfo.size
val total = scrollInfo.totalItemsCount
// derivedStateOf — ไม่คอมโพสใหม่หากค่าไม่เปลี่ยนแปลง
val scrollProgress by remember {
derivedStateOf {
if (total > 0) visibleCount.toFloat() / total else 0f
}
}
LinearProgressIndicator(
progress = scrollProgress,
modifier = Modifier.fillMaxWidth().height(2.dp)
)
}
ข้อผิดพลาดทั่วไป: (1) ไม่มี key — องค์ประกอบจะถูกคอมโพสใหม่เมื่อมีการเปลี่ยนแปลงรายการใดๆ (2) ใช้ mutableStateListOf โดยไม่มี snapshot flows — การเปลี่ยนแปลงอาจไม่ถูกติดตาม (3) การเรียกฟังก์ชัน suspend ภายใน itemContent — ขัดจังหวะคอมโพซิชัน (4) ไม่มี contentType สำหรับประเภทผสม — Compose นำคอมโพซิชันของโฆษณามาใช้สำหรับโพสต์ข้อความ ทำให้เกิดสิ่งแปลกปลอมทางภาพ ที่ IT Sectr เราเพิ่ม contentType ในทุกรายการที่มีเนื้อหาสามประเภทขึ้นไป
คำถามที่พบบ่อย
ตรวจสอบสามพารามิเตอร์: (1) key — หากไม่มีคีย์ที่เสถียร Compose ไม่สามารถนำคอมโพซิชันกลับมาใช้ใหม่ได้ (2) contentType — สำหรับรายการที่มีประเภทองค์ประกอบแตกต่างกัน (3) beyondBoundsPageCount — เพิ่มเป็น 2 สำหรับรูปภาพ ย้ายการคำนวณหนักภายใน itemContent ไปยัง remember พร้อมคีย์ตามข้อมูล ใช้ Modifier.drawWithContent แทน Image สำหรับกราฟิกง่ายๆ
ใช้ LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) สำหรับระยะห่างระหว่างองค์ประกอบ สำหรับตัวคั่นภาพที่มีเส้น ให้เพิ่ม item { Divider() } ระหว่าง items ตัวคั่นอัตโนมัติ: items(items, key = { it.id }) { item -> ... } และระหว่างแต่ละองค์ประกอบ ให้แทรก item { HorizontalDivider() } สำหรับการนี้ ให้ใช้ LazyListScope.items(list, key) { /* องค์ประกอบ */ } และแยก LazyListScope.item { Divider() }
LazyColumn มีความสูงไม่จำกัด (fillMaxSize) โดยค่าเริ่มต้น ภายใน Column ที่ไม่มีความสูงคงที่ LazyColumn ไม่สามารถกำหนดขนาดของมันได้ วิธีแก้ไข: (1) กำหนดความสูงคงที่ให้ LazyColumn: Modifier.height(400.dp), (2) ใช้ Modifier.weight(1f) ภายใน Column, (3) อย่าวาง LazyColumn ซ้อนภายในคอนเทนเนอร์ที่เลื่อนแนวตั้งได้ — ใช้ LazyColumn เป็นองค์ประกอบรากพร้อม item {} สำหรับหัวเรื่องและส่วนท้าย
LazyColumn ตอบสนองต่อการเปลี่ยนแปลง State โดยอัตโนมัติ หากข้อมูลถูกเก็บใน mutableStateListOf หรือ mutableStateOf Compose จะคอมโพสเฉพาะองค์ประกอบที่เปลี่ยนแปลง สำหรับรายการจาก ViewModel ให้ใช้ collectAsState() กับ Flow เมื่ออัปเดตรายการด้วย key Compose จะสร้างแอนิเมชันการเปลี่ยนแปลง (การเพิ่ม, การลบ, การเรียงลำดับใหม่) โดยอัตโนมัติผ่าน Modifier.animateItemPlacement()
LazyColumn — สำหรับรายการคอลัมน์เดียว (ฟีด, แชท, ความคิดเห็น) LazyVerticalGrid — สำหรับกริด (แกลเลอรี, แคตตาล็อก, ไอคอน) หากองค์ประกอบควรอยู่ในหนึ่งคอลัมน์บนโทรศัพท์และสองคอลัมน์บนแท็บเล็ต — ใช้ LazyVerticalGrid กับ GridCells.Adaptive หากต้องการกริดแบบผสม (จำนวนคอลัมน์ต่างกันในส่วนเดียว) — ใช้ LazyColumn และแทรก LazyRow แนวนอนหรือ LazyVerticalGrid ที่ซ้อนอยู่ภายใน items
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ