ListView là thành phần Android cơ bản để hiển thị danh sách dọc, có sẵn từ API 1 (Android 1.0). ListView tạo danh sách các mục bằng ArrayAdapter và hỗ trợ bố cục ô tùy chỉnh. Hãy tìm hiểu ListView là gì, cách cấu hình nó và những cạm bẫy mà thành phần cũ nhưng vẫn được sử dụng này ẩn giấu. Tại IT Sectr, chúng tôi gặp ListView trong các dự án kế thừa và duy trì khả năng tương thích ngược với Android 4.4. Để so sánh với giải pháp thay thế hiện đại, hãy đọc bài viết về RecyclerView.
Những điểm chính
ListView là một ViewGroup từ gói android.widget, được thiết kế để hiển thị danh sách các mục có thể cuộn. ListView triển khai mẫu AdapterView: dữ liệu được truyền qua một bộ điều hợp (Adapter), chuyển đổi các mục dữ liệu thành Views hiển thị trên màn hình. ListView đã tồn tại trong Android SDK từ phiên bản đầu tiên (API 1) và không yêu cầu thư viện bổ sung.
Kiến trúc của ListView đơn giản hơn RecyclerView: LayoutManager được tích hợp sẵn (chỉ cuộn dọc), không có hoạt ảnh thay đổi, dấu phân cách được đặt qua thuộc tính XML android:divider. ListView tự động thêm OverScrollView và đánh dấu tiêu điểm, giúp đơn giản hóa việc tạo nguyên mẫu. Theo Statista (2025), khoảng 15% ứng dụng Android trong top 100 Play Store vẫn chứa ListView — chủ yếu là các dự án kế thừa với hỗ trợ tối thiểu.
ListView là thành phần danh sách chính trong Android từ 2008 đến 2014, khi RecyclerView được giới thiệu tại Google I/O 2014. Mặc dù đã bị lỗi thời một phần (soft-deprecated), ListView vẫn khả dụng trong tất cả các phiên bản Android và không bị đánh dấu @Deprecated trong SDK API 35. Nếu dự án của bạn hỗ trợ Android 4.4 (API 19) trở xuống, ListView là lựa chọn tích hợp duy nhất vì RecyclerView yêu cầu AndroidX và phụ thuộc Gradle.
ArrayAdapter là bộ điều hợp phổ biến nhất cho ListView. Nó nhận List<T> và bố cục cho mỗi mục. Theo mặc định, ArrayAdapter gọi toString() trên mỗi đối tượng và hiển thị văn bản trong simple_list_item_1 (một TextView duy nhất). Để hiển thị tùy chỉnh, bạn cần ghi đè getView() hoặc truyền bố cục tùy chỉnh cho hàm tạo.
// ListView với ArrayAdapter (văn bản đơn giản)
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val countries = listOf(
"Nga", "Mỹ", "Đức",
"Pháp", "Nhật Bản", "Trung Quốc", "Brazil"
)
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
// Xử lý nhấp chuột
binding.listView.onItemClickListener =
AdapterView.OnItemClickListener { _, _, position, _ ->
Toast.makeText(this, countries[position], Toast.LENGTH_SHORT).show()
}
}
}
SimpleAdapter được thiết kế cho danh sách từ dữ liệu Map. Nó nhận List<Map<String, Any>> và mảng from/to để liên kết khóa Map với ID widget trong bố cục. SimpleAdapter thuận tiện cho nguyên mẫu nhanh khi không cần lớp bộ điều hợp riêng. Nhược điểm là thiếu kiểu an toàn và phức tạp với logic tùy chỉnh.
<!-- Bố cục 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>
Bộ điều hợp tùy chỉnh cho ListView ghi đè phương thức getView(), mà RecyclerView chia thành onCreateViewHolder + onBindViewHolder. Trong ListView, cả hai bước được kết hợp, do đó nhà phát triển phải triển khai độc lập mẫu ViewHolder qua convertView (View có thể tái sử dụng) và phương thức setTag().
// Mô hình dữ liệu
data class Product(
name: String,
price: String,
imageResId: Int
)
// Bộ điều hợp tùy chỉnh với ViewHolder
class ProductAdapter(
context: Context,
private val products: List<Product>
) : ArrayAdapter<Product>(context, 0, products) {
// ViewHolder để lưu cache
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
}
}
Sự khác biệt chính với RecyclerView: trong ListView, phương thức getView() được gọi mỗi khi một mục xuất hiện trên màn hình. Nếu không sử dụng convertView và ViewHolder, mỗi lần cuộn tạo ra một đối tượng View mới, dẫn đến rò rỉ bộ nhớ và độ trễ trên danh sách hơn 50 mục. ViewHolder với view.tag là tối ưu hóa bắt buộc cho ListView với bố cục tùy chỉnh.
ListView có bốn hạn chế về kiến trúc mà Google đã khắc phục trong RecyclerView: (1) chỉ hướng dọc — không có lưới, danh sách ngang hoặc tầng; (2) không có thông báo chi tiết — bất kỳ thay đổi dữ liệu nào cũng kích hoạt vẽ lại toàn bộ qua notifyDataSetChanged(), gây giật; (3) không có hoạt ảnh thêm/xóa mục tích hợp; (4) không có phân chia trách nhiệm — LayoutManager được tích hợp, ItemAnimator không có.
<!-- item_product.xml — bố cục mục 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>
Ý nghĩa thực tế: trên danh sách lên đến 20 mục, ListView hoạt động nhanh như RecyclerView. Ở 100+ mục, sự khác biệt trở nên rõ rệt: ListView bắt đầu giảm FPS do thiếu tính toán diff và ViewHolder bắt buộc. Đối với danh sách cập nhật thường xuyên (chat, feed, log), ListView không phù hợp — hãy sử dụng RecyclerView với Paging 3. Tại IT Sectr, chúng tôi di chuyển ListView sang RecyclerView mỗi khi làm việc trên màn hình kế thừa có hơn 50 mục.
ListView cung cấp một tập hợp các thuộc tính XML để cấu hình nhanh không cần mã: android:divider (màu/drawable dấu phân cách), android:dividerHeight (độ dày), android:scrollbars (thanh cuộn), android:fastScrollEnabled (cuộn nhanh qua tay cầm), android:choiceMode (chế độ chọn: singleChoice, multipleChoice). Thuộc tính android:entries cho phép điền danh sách từ mảng tài nguyên (string-array) mà không cần bộ điều hợp.
| Thuộc tính | Mô tả | Giá trị ví dụ |
|---|---|---|
| android:divider | Màu hoặc drawable dấu phân cách | @color/gray_light |
| android:dividerHeight | Độ dày dấu phân cách tính bằng px/dp | 1dp |
| android:entries | Mảng chuỗi từ tài nguyên | @array/countries |
| android:choiceMode | Chế độ chọn mục | singleChoice |
| android:fastScrollEnabled | Cuộn nhanh | true |
| android:overlapAnchor | Neo cho danh sách bật lên (API 21+) | true |
Chế độ chọn: choiceMode="singleChoice" bật kiểu RadioButton nơi mục được chọn được đánh dấu. choiceMode="multipleChoice" — hộp kiểm cho nhiều mục. Dữ liệu chọn có sẵn qua listView.checkedItemPosition hoặc listView.checkedItemIds. Quan trọng: với choiceMode, ListView sử dụng StateListDrawable cho nền — hãy đặt bộ chọn của riêng bạn nếu màu xanh tiêu chuẩn không phù hợp với thiết kế.
Sự lựa chọn giữa ListView và RecyclerView phụ thuộc vào yêu cầu dự án. RecyclerView là tiêu chuẩn hiện đại, nhưng ListView có thể hợp lý cho các màn hình đơn giản với chi phí bảo trì mã kế thừa tối thiểu. So sánh trên tất cả các tham số chính:
| Tham số | ListView | RecyclerView |
|---|---|---|
| Kích thước thư viện | Tích hợp trong SDK (≈60 KB) | AndroidX (≈300 KB với phụ thuộc) |
| Phiên bản tối thiểu | API 1 | API 14 (qua AndroidX) |
| Hướng | Chỉ dọc | Dọc, ngang, lưới, so le |
| ViewHolder | Triển khai thủ công | Bắt buộc theo kiến trúc |
| Hoạt ảnh | Chỉ chuyển tiếp mờ dần | DefaultItemAnimator (add, remove, move, change) |
| Cập nhật chi tiết | Không (chỉ notifyDataSetChanged) | notifyItemInserted/Removed/Changed |
| Trình nghe nhấp | onItemClickListener (tích hợp) | Qua ViewHolder (không tích hợp) |
| Hiệu suất | Đủ cho danh sách lên đến 50 mục | 60 FPS ổn định cho 1000+ mục |
Kết luận: cho dự án mới, hãy sử dụng RecyclerView. Chỉ giữ ListView trong mã kế thừa nơi việc di chuyển quá tốn kém. Theo hướng dẫn Material Design 3 (2026), danh sách ở mọi độ phức tạp nên sử dụng RecyclerView để đáp ứng các tiêu chuẩn hiệu suất và khả năng tiếp cận hiện đại.
Câu hỏi thường gặp
ListView không bị đánh dấu @Deprecated trong Android SDK API 35, nhưng Google không khuyến nghị sử dụng nó trong các dự án mới (soft-deprecated). Thành phần này vẫn hoạt động trong tất cả các phiên bản Android. RecyclerView là sự thay thế chính thức từ năm 2014. Nếu bạn bắt đầu một dự án mới — hãy chọn RecyclerView. Nếu bạn đang duy trì dự án hiện có — ListView sẽ tiếp tục hoạt động mà không thay đổi.
ListView hỗ trợ các phương thức addHeaderView(View, Object, boolean) và addFooterView(View, Object, boolean). Header và footer được thêm vào danh sách mục chung nhưng không tham gia vào bộ điều hợp — chúng luôn được hiển thị ở đầu và cuối danh sách. Quan trọng: thêm header/footer trước khi đặt bộ điều hợp (setAdapter()), nếu không chúng sẽ không được bao gồm. Sau khi thêm header, ID vị trí của các mục bị dịch chuyển theo số lượng header.
Cả ListView và ScrollView đều xử lý cử chỉ dọc, tạo ra xung đột: ScrollView bên ngoài chặn cuộn và ListView không cuộn được. Giải pháp: (1) không lồng ListView bên trong ScrollView — sử dụng RecyclerView với NestedScrollingChild, (2) thay thế ScrollView bằng NestedScrollView (API 21+), (3) sử dụng ListView với chiều cao cố định (android:layout_height="...dp") bên trong ScrollView, nhưng điều này sẽ phá vỡ khả năng thích ứng.
Việc xóa dữ liệu trong ListView phụ thuộc vào loại bộ điều hợp. Đối với ArrayAdapter: adapter.clear() và adapter.addAll(newList) sau đó adapter.notifyDataSetChanged(). Đối với bộ điều hợp tùy chỉnh: cập nhật danh sách dữ liệu và gọi notifyDataSetChanged(). Lưu ý: listView.invalidateViews() chỉ vẽ lại Views nhưng không cập nhật dữ liệu. Luôn thay đổi dữ liệu qua bộ điều hợp, không trực tiếp.
Chiều cao của mục ListView được đặt trong bố cục mục. Nếu bố cục sử dụng layout_height="wrap_content", chiều cao sẽ điều chỉnh theo nội dung. Để có chiều cao cố định cho tất cả các mục, hãy sử dụng bộ điều hợp tùy chỉnh đặt LayoutParams trong getView(): view.layoutParams.height = resources.getDimensionPixelSize(R.dimen.itemHeight). Đối với chiều cao khác nhau, hãy sử dụng RecyclerView với các loại view khác nhau.
Tóm tắt
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm