ListView เป็นคอมโพเนนต์พื้นฐานของ Android สำหรับแสดงรายการแนวตั้ง ตั้งแต่ API 1 (Android 1.0) ListView สร้างรายการของไอเท็มโดยใช้ ArrayAdapter และรองรับเค้าโครงเซลล์ที่กำหนดเอง มาทำความเข้าใจว่า ListView คืออะไร วิธีการกำหนดค่า และข้อผิดพลาดที่คอมโพเนนต์เก่าแต่ยังคงใช้งานนี้ซ่อนอยู่ ที่ IT Sectr เราพบ ListView ในโปรเจกต์เดิมและรักษาความเข้ากันได้ย้อนหลังกับ Android 4.4 สำหรับการเปรียบเทียบกับทางเลือกสมัยใหม่อ่าน บทความเกี่ยวกับ RecyclerView
ประเด็นสำคัญ
ListView คือ ViewGroup จากแพ็คเกจ android.widget ที่ออกแบบมาเพื่อแสดงรายการไอเท็มที่เลื่อนได้ ListView ใช้งานรูปแบบ AdapterView: ข้อมูลจะถูกส่งผ่านอะแดปเตอร์ (Adapter) ซึ่งแปลงไอเท็มข้อมูลเป็น Views ที่แสดงบนหน้าจอ ListView มีอยู่ใน Android SDK ตั้งแต่เวอร์ชันแรก (API 1) และไม่ต้องใช้ไลบรารีเพิ่มเติม
สถาปัตยกรรม ของ ListView ง่ายกว่า RecyclerView: LayoutManager ในตัว (เลื่อนแนวตั้งเท่านั้น) ไม่มีแอนิเมชันการเปลี่ยนแปลง ตัวแบ่งถูกตั้งค่าผ่านแอตทริบิวต์ XML android:divider ListView เพิ่ม OverScrollView และการเน้นโฟกัสโดยอัตโนมัติ ซึ่งช่วยทำให้การสร้างต้นแบบง่ายขึ้น ตามข้อมูลของ Statista (2025) ประมาณ 15% ของแอป Android ใน 100 อันดับแรกของ Play Store ยังคงมี ListView — ส่วนใหญ่เป็นโปรเจกต์เดิมที่มีการสนับสนุนน้อยที่สุด
ListView เป็นคอมโพเนนต์รายการหลักใน Android ตั้งแต่ปี 2008 ถึง 2014 เมื่อ RecyclerView ถูกนำเสนอที่ Google I/O 2014 แม้จะเลิกใช้บางส่วน (soft-deprecated) แต่ ListView ยังคงใช้ได้ใน Android ทุกรุ่นและไม่ได้ถูกทำเครื่องหมายเป็น @Deprecated ใน SDK API 35 หากโปรเจกต์ของคุณรองรับ Android 4.4 (API 19) และต่ำกว่า ListView เป็นตัวเลือกในตัวเดียวเนื่องจาก RecyclerView ต้องใช้ AndroidX และการพึ่งพา Gradle
ArrayAdapter เป็นอะแดปเตอร์ที่ได้รับความนิยมมากที่สุดสำหรับ ListView โดยรับ List<T> และเค้าโครงสำหรับแต่ละไอเท็ม โดยค่าเริ่มต้น ArrayAdapter เรียก toString() บนแต่ละออบเจ็กต์และแสดงข้อความใน simple_list_item_1 (TextView เดียว) สำหรับการแสดงผลแบบกำหนดเอง คุณต้องแทนที่ getView() หรือส่งเค้าโครงที่กำหนดเองไปยังคอนสตรักเตอร์
// ListView กับ ArrayAdapter (ข้อความอย่างง่าย)
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val countries = listOf(
"รัสเซีย", "สหรัฐอเมริกา", "เยอรมนี",
"ฝรั่งเศส", "ญี่ปุ่น", "จีน", "บราซิล"
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val adapter = ArrayAdapter<String>(
this,
android.R.layout.simple_list_item_1,
countries
)
binding.listView.adapter = adapter
// การจัดการคลิก
binding.listView.onItemClickListener =
AdapterView.OnItemClickListener { _, _, position, _ ->
Toast.makeText(this, countries[position], Toast.LENGTH_SHORT).show()
}
}
}
SimpleAdapter ออกแบบมาสำหรับรายการจากข้อมูล Map โดยรับ List<Map<String, Any>> และอาร์เรย์ from/to สำหรับเชื่อมโยงคีย์ Map กับ ID ของวิดเจ็ตในเค้าโครง SimpleAdapter สะดวกสำหรับต้นแบบด่วนเมื่อไม่ต้องการคลาสอะแดปเตอร์แยกต่างหาก ข้อเสียคือขาดการพิมพ์ที่ปลอดภัยและความซับซ้อนกับตรรกะที่กำหนดเอง
<!-- เค้าโครง ListView -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@color/divider"
android:dividerHeight="1px"
android:scrollbars="vertical" />
</LinearLayout>
อะแดปเตอร์ที่กำหนดเอง สำหรับ ListView จะแทนที่เมธอด getView() ซึ่ง RecyclerView แบ่งเป็น onCreateViewHolder + onBindViewHolder ใน ListView ทั้งสองขั้นตอนรวมกัน ดังนั้นนักพัฒนาต้องดำเนินการรูปแบบ ViewHolder อย่างอิสระผ่าน convertView (View ที่ใช้ซ้ำได้) และเมธอด setTag()
// โมเดลข้อมูล
data class Product(
name: String,
price: String,
imageResId: Int
)
// อะแดปเตอร์ที่กำหนดเองกับ ViewHolder
class ProductAdapter(
context: Context,
private val products: List<Product>
) : ArrayAdapter<Product>(context, 0, products) {
// ViewHolder สำหรับแคช
private class ViewHolder(view: View) {
val nameText: TextView = view.findViewById(R.id.tvName)
val priceText: TextView = view.findViewById(R.id.tvPrice)
val imageView: ImageView = view.findViewById(R.id.ivProduct)
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(context)
.inflate(R.layout.item_product, parent, false)
.also { it.tag = ViewHolder(it) }
val holder = view.tag as ViewHolder
val product = getItem(position)
holder.nameText.text = product.name
holder.priceText.text = product.price
holder.imageView.setImageResource(product.imageResId)
return view
}
}
ความแตกต่างหลักจาก RecyclerView: ใน ListView เมธอด getView() ถูกเรียกทุกครั้งที่ไอเท็มปรากฏบนหน้าจอ หากไม่ใช้ convertView และ ViewHolder การเลื่อนแต่ละครั้งจะสร้างออบเจ็กต์ View ใหม่ ทำให้เกิดหน่วยความจำรั่วและความหน่วงในรายการที่มี มากกว่า 50 ไอเท็ม ViewHolder ด้วย view.tag เป็นการเพิ่มประสิทธิภาพที่จำเป็นสำหรับ ListView ที่มีเค้าโครงแบบกำหนดเอง
ListView มีข้อจำกัดทางสถาปัตยกรรมสี่ประการที่ Google แก้ไขใน RecyclerView: (1) แนวตั้งเท่านั้น — ไม่มีกริด รายการแนวนอน หรือแบบเรียงซ้อน; (2) ไม่มีการแจ้งเตือนแบบละเอียด — การเปลี่ยนแปลงข้อมูลใด ๆ จะทำให้วาดใหม่ทั้งหมดผ่าน notifyDataSetChanged() ทำให้เกิดการกระตุก; (3) ไม่มีแอนิเมชันการเพิ่ม/ลบไอเท็มในตัว; (4) ไม่มีการแยกหน้าที่ — LayoutManager ในตัว ItemAnimator ไม่มี
<!-- item_product.xml — เค้าโครงไอเท็ม ListView -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp">
<ImageView
android:id="@+id/ivProduct"
android:layout_width="56dp"
android:layout_height="56dp"
android:scaleType="centerCrop" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingStart="12dp">
<TextView
android:id="@+id/tvName"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvPrice"
android:textSize="14sp"
android:textColor="@color/price" />
</LinearLayout>
</LinearLayout>
ผลกระทบในทางปฏิบัติ: ในรายการสูงสุด 20 ไอเท็ม ListView ทำงานเร็วเท่ากับ RecyclerView ที่ 100+ ไอเท็ม ความแตกต่างจะเห็นได้ชัด: ListView เริ่ม FPS ตกเนื่องจากขาดการคำนวณ diff และ ViewHolder ที่ถูกบังคับ สำหรับรายการที่อัปเดตบ่อย (แชท ฟีด บันทึก) ListView ไม่เหมาะสม — ใช้ RecyclerView กับ Paging 3 ที่ IT Sectr เราย้าย ListView ไปยัง RecyclerView เมื่อทำงานกับหน้าจอเดิมที่มีมากกว่า 50 ไอเท็ม
ListView มีชุดแอตทริบิวต์ XML สำหรับการกำหนดค่าอย่างรวดเร็วโดยไม่ต้องเขียนโค้ด: android:divider (สี/ดรอว์เอเบิลตัวแบ่ง), android:dividerHeight (ความหนา), android:scrollbars (แถบเลื่อน), android:fastScrollEnabled (เลื่อนเร็วผ่านแฮนเดิล), android:choiceMode (โหมดการเลือก: singleChoice, multipleChoice) แอตทริบิวต์ android:entries อนุญาตให้เติมรายการจากอาร์เรย์ทรัพยากร (string-array) โดยไม่ต้องใช้อะแดปเตอร์
| แอตทริบิวต์ | คำอธิบาย | ค่าตัวอย่าง |
|---|---|---|
| android:divider | สีหรือดรอว์เอเบิลตัวแบ่ง | @color/gray_light |
| android:dividerHeight | ความหนาของตัวแบ่งใน px/dp | 1dp |
| android:entries | อาร์เรย์สตริงจากทรัพยากร | @array/countries |
| android:choiceMode | โหมดการเลือกไอเท็ม | singleChoice |
| android:fastScrollEnabled | การเลื่อนอย่างรวดเร็ว | true |
| android:overlapAnchor | จุดยึดสำหรับรายการป๊อปอัป (API 21+) | true |
โหมดการเลือก: choiceMode="singleChoice" เปิดใช้งานสไตล์ RadioButton โดยที่ไอเท็มที่เลือกจะถูกเน้น choiceMode="multipleChoice" — กล่องกาเครื่องหมายสำหรับหลายไอเท็ม ข้อมูลการเลือกสามารถเข้าถึงได้ผ่าน listView.checkedItemPosition หรือ listView.checkedItemIds สำคัญ: เมื่อใช้ choiceMode ListView ใช้ StateListDrawable สำหรับพื้นหลัง — ตั้งค่าตัวเลือกของคุณเองหากสีน้ำเงินมาตรฐานไม่ตรงกับการออกแบบ
การเลือก ระหว่าง ListView และ RecyclerView ขึ้นอยู่กับความต้องการของโปรเจกต์ RecyclerView เป็นมาตรฐานสมัยใหม่ แต่ ListView อาจเหมาะสมสำหรับหน้าจอเรียบง่ายที่มีค่าใช้จ่ายในการบำรุงรักษาโค้ดเดิมน้อยที่สุด การเปรียบเทียบพารามิเตอร์หลักทั้งหมด:
| พารามิเตอร์ | ListView | RecyclerView |
|---|---|---|
| ขนาดไลบรารี | ในตัวใน SDK (≈60 KB) | AndroidX (≈300 KB พร้อมการพึ่งพา) |
| เวอร์ชันขั้นต่ำ | API 1 | API 14 (ผ่าน AndroidX) |
| ทิศทาง | แนวตั้งเท่านั้น | แนวตั้ง แนวนอน กริด แบบเรียงซ้อน |
| ViewHolder | ดำเนินการด้วยตนเอง | จำเป็นตามสถาปัตยกรรม |
| แอนิเมชัน | เฉพาะการเปลี่ยนแบบเฟด | DefaultItemAnimator (add, remove, move, change) |
| การอัปเดตแบบละเอียด | ไม่ (เฉพาะ notifyDataSetChanged) | notifyItemInserted/Removed/Changed |
| ตัวฟังคลิก | onItemClickListener (ในตัว) | ผ่าน ViewHolder (ไม่มีในตัว) |
| ประสิทธิภาพ | เพียงพอสำหรับรายการสูงสุด 50 ไอเท็ม | 60 FPS ที่เสถียรสำหรับ 1000+ ไอเท็ม |
สรุป: สำหรับโปรเจกต์ใหม่ ใช้ RecyclerView ปล่อย ListView ไว้ในโค้ดเดิมที่การย้ายข้อมูลมีค่าใช้จ่ายสูงเกินไปเท่านั้น ตามแนวทาง Material Design 3 (2026) รายการทุกความซับซ้อนควรใช้ RecyclerView เพื่อให้เป็นไปตามมาตรฐานประสิทธิภาพและการเข้าถึงที่ทันสมัย
คำถามที่พบบ่อย
ListView ไม่ได้ถูกทำเครื่องหมายเป็น @Deprecated ใน Android SDK API 35 แต่ Google ไม่แนะนำให้ใช้ในโปรเจกต์ใหม่ (soft-deprecated) คอมโพเนนต์ยังคงทำงานได้ใน Android ทุกรุ่น RecyclerView เป็นตัวแทนอย่างเป็นทางการตั้งแต่ปี 2014 หากคุณเริ่มโปรเจกต์ใหม่ — เลือก RecyclerView หากคุณดูแลโปรเจกต์ที่มีอยู่ — ListView จะทำงานต่อไปโดยไม่เปลี่ยนแปลง
ListView รองรับเมธอด addHeaderView(View, Object, boolean) และ addFooterView(View, Object, boolean) ส่วนหัวและส่วนท้ายจะถูกเพิ่มในรายการไอเท็มทั่วไปแต่ไม่ได้มีส่วนร่วมในอะแดปเตอร์ — จะแสดงที่จุดเริ่มต้นและจุดสิ้นสุดของรายการเสมอ สำคัญ: เพิ่ม header/footer ก่อนตั้งค่าอะแดปเตอร์ (setAdapter()) มิฉะนั้นจะไม่ถูกรวม หลังจากเพิ่ม header แล้ว ID ตำแหน่งของไอเท็มจะเลื่อนตามจำนวนส่วนหัว
ทั้ง ListView และ ScrollView จัดการท่าทางแนวตั้ง ซึ่งสร้างความขัดแย้ง: ScrollView ภายนอกดักจับการเลื่อน และ ListView ไม่เลื่อน วิธีแก้ไข: (1) อย่าวาง ListView ซ้อนใน ScrollView — ใช้ RecyclerView กับ NestedScrollingChild, (2) แทนที่ ScrollView ด้วย NestedScrollView (API 21+), (3) ใช้ ListView ที่มีความสูงคงที่ (android:layout_height="...dp") ภายใน ScrollView แต่จะทำให้การปรับเปลี่ยนเสีย
การล้างข้อมูลใน ListView ขึ้นอยู่กับประเภทของอะแดปเตอร์ สำหรับ ArrayAdapter: adapter.clear() และ adapter.addAll(newList) ตามด้วย adapter.notifyDataSetChanged() สำหรับอะแดปเตอร์ที่กำหนดเอง: อัปเดตรายการข้อมูลและเรียก notifyDataSetChanged() หมายเหตุ: listView.invalidateViews() วาด Views ใหม่เท่านั้นแต่ไม่อัปเดตข้อมูล เปลี่ยนข้อมูลผ่านอะแดปเตอร์เสมอ ไม่ใช่โดยตรง
ความสูงของไอเท็ม ListView ถูกกำหนดในเค้าโครงของไอเท็ม หากเค้าโครงใช้ layout_height="wrap_content" ความสูงจะปรับตามเนื้อหา สำหรับความสูงคงที่สำหรับไอเท็มทั้งหมด ให้ใช้อะแดปเตอร์ที่กำหนดเองซึ่งตั้งค่า LayoutParams ใน getView(): view.layoutParams.height = resources.getDimensionPixelSize(R.dimen.itemHeight) สำหรับความสูงที่แตกต่างกัน ให้ใช้ RecyclerView ที่มีประเภทวิวต่างกัน
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ