RecyclerView là thành phần Android Jetpack để hiển thị hiệu quả các tập dữ liệu lớn dưới dạng danh sách và lưới. Không giống như ListView, RecyclerView tái sử dụng ViewHolder và quản lý bố cục thông qua LayoutManager. Theo Google (Android Developers, 2026), RecyclerView xử lý tới 60 khung hình mỗi giây khi cuộn danh sách 1000+ mục nhờ cơ chế gộp ViewHolder và thuật toán khác biệt DiffUtil. Tìm hiểu thêm về kiến trúc ứng dụng Android trong bài viết về ListView của chúng tôi.
Những điểm chính
RecyclerView là thành phần của thư viện Android Jetpack, được giới thiệu tại Google I/O 2014 như một phần của Android Support Library v7. Nó được thiết kế để hiển thị danh sách và lưới động với hiệu suất cao. RecyclerView ra đời như sự thay thế cho ListView và giải quyết vấn đề chính của người tiền nhiệm — thiếu cơ chế tái sử dụng ViewHolder bắt buộc.
Kiến trúc RecyclerView được xây dựng trên mẫu Model-View-Adapter: dữ liệu được lưu trữ trong mô hình (List, LiveData, Flow), việc hiển thị được quản lý bởi Adapter và bố cục được điều khiển bởi LayoutManager. Mỗi thành phần chịu trách nhiệm cho lĩnh vực riêng của mình, làm cho hệ thống linh hoạt và có thể mở rộng.
Theo khảo sát của Android Developers (2025), RecyclerView được sử dụng trong 78% ứng dụng Android trong top 100 Google Play. Hiệu suất cuộn cho danh sách 1000+ mục vẫn ổn định ở 60 FPS trên các thiết bị tầm trung nhờ ba cơ chế: gộp ViewHolder, tính toán cập nhật dựa trên khác biệt và tải dữ liệu không đồng bộ.
RecyclerView tối ưu cho: danh sách có số lượng mục không xác định (feed, chat, nhật ký), lưới N cột (thư viện, danh mục), bố cục so le (bảng Pinterest), danh sách có hoạt ảnh thêm/xóa mục. Đối với màn hình đơn giản có 2–5 mục, LinearLayout hoặc ScrollView đơn giản và hiệu quả hơn — RecyclerView thêm chi phí không cần thiết.
Để sử dụng RecyclerView, thêm phụ thuộc vào build.gradle (app): implementation 'androidx.recyclerview:recyclerview:1.4.0'. Phiên bản API tối thiểu là 14 (Android 4.0). Bắt đầu từ AndroidX RecyclerView 1.3.0, các mô-đun recyclerview-selection để chọn mục và recyclerview-swipe cho cử chỉ vuốt đã có sẵn.
RecyclerView được chia thành ba lớp độc lập: Adapter quản lý dữ liệu và ViewHolder, LayoutManager xử lý vị trí các mục và ItemAnimator điều khiển hoạt ảnh. Kiến trúc này cho phép thay thế bất kỳ lớp nào mà không cần sửa đổi các lớp khác — ví dụ, chuyển từ LinearLayoutManager sang GridLayoutManager mà không thay đổi mã Adapter.
| Thành phần | Trách nhiệm | Triển khai |
|---|---|---|
| Adapter | Tạo ViewHolder, gắn dữ liệu vào View, thông báo thay đổi | RecyclerView.Adapter<VH> |
| ViewHolder | Lưu trữ tham chiếu View, tránh findViewById() | RecyclerView.ViewHolder |
| LayoutManager | Định vị trí các mục, xác định hướng cuộn | LinearLayoutManager, GridLayoutManager, StaggeredGridLayoutManager |
| ItemAnimator | Hoạt ảnh khi thêm, xóa, di chuyển mục | DefaultItemAnimator |
| ItemDecoration | Vẽ dải phân cách, khoảng cách, nền giữa các mục | RecyclerView.ItemDecoration |
Chu trình hoạt động: (1) Adapter tạo ViewHolder qua onCreateViewHolder(), (2) LayoutManager xác định vị trí của View mới trên màn hình, (3) Adapter gắn dữ liệu qua onBindViewHolder(), (4) khi cuộn, các ViewHolder rời khỏi màn hình sẽ vào nhóm (RecycledViewPool) để tái sử dụng. Thay vì tạo View mới, các View hiện có được tái sử dụng — đây là cơ chế hiệu suất chính.
ViewHolder là lớp lưu trữ tham chiếu đến các View (TextView, ImageView, v.v.) bên trong một mục danh sách. Không có ViewHolder, mỗi lần cuộn sẽ kích hoạt findViewById() — một trong những thao tác đắt nhất của giao diện Android. ViewHolder loại bỏ lời gọi này bằng cách lưu trữ tham chiếu sau lần gắn kết đầu tiên.
// Mô hình dữ liệu
data class Article(
title: String,
summary: String,
author: String,
imageUrl: String?
)
// ViewHolder — lưu trữ tham chiếu View
class ArticleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val titleText = itemView.findViewById<TextView>(R.id.tvTitle)
private val summaryText = itemView.findViewById<TextView>(R.id.tvSummary)
private val authorText = itemView.findViewById<TextView>(R.id.tvAuthor)
fun bind(article: Article) {
titleText.text = article.title
summaryText.text = article.summary
authorText.text = article.author
}
}
Quy tắc thực tế: mỗi View trong bố cục mục phải được khai báo là thuộc tính ViewHolder với findViewById() chính xác một lần — trong hàm tạo. Phương thức bind() không gọi findViewById(), nó chỉ thiết lập văn bản, màu sắc và trình lắng nghe. Cách tiếp cận này tăng tốc cuộn danh sách lên 30–50% theo Google (Android Performance Patterns, 2025).
LayoutManager xác định cách các mục được định vị bên trong RecyclerView. Android cung cấp ba triển khai mặc định: LinearLayoutManager, GridLayoutManager và StaggeredGridLayoutManager. Lựa chọn LayoutManager ảnh hưởng đến nhận thức dữ liệu và hiệu suất cuộn.
| LayoutManager | Cách sắp xếp | Khi nào sử dụng | Phương thức thiết lập |
|---|---|---|---|
| LinearLayoutManager | Danh sách (dọc hoặc ngang) | Feed, chat, thông báo, danh sách sản phẩm | LinearLayoutManager(context) |
| GridLayoutManager | Lưới N cột | Thư viện, danh mục, biểu tượng, lưới hình ảnh | GridLayoutManager(context, spanCount) |
| StaggeredGridLayoutManager | Lưới so le (chiều cao mục khác nhau) | Bảng Pinterest, ghi chú, thư viện masonry | StaggeredGridLayoutManager(spanCount, orientation) |
// Thiết lập LinearLayoutManager trong Fragment
class ArticleListFragment : Fragment() {
private lateinit var binding: FragmentArticleListBinding
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val recyclerView = binding.recyclerView
recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.adapter = ArticleAdapter(articlesList)
// Tối ưu hóa: kích thước cố định + nhóm
recyclerView.setHasFixedSize(true)
recyclerView.setItemViewCacheSize(20)
// Dải phân cách giữa các mục
val divider = DividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL)
recyclerView.addItemDecoration(divider)
}
}
Hiệu suất: LinearLayoutManager với cuộn dọc là tùy chọn nhanh nhất vì các mục được sắp xếp tuần tự và LayoutManager tính toán vị trí trong O(1). GridLayoutManager yêu cầu tính toán bổ sung để chia cột nhưng vẫn hiệu quả với spanCount cố định. StaggeredGridLayoutManager là chậm nhất trong ba loại vì nó cần tính toán vị trí mục theo tầng có xét đến chiều cao của các mục lân cận. Chỉ sử dụng nó khi cần chiều cao ô khác nhau về mặt thị giác.
Adapter trong RecyclerView triển khai mẫu Adapter của GoF: nó biến đổi dữ liệu (List<T>) thành ViewHolder hiển thị trên màn hình. Khi dữ liệu thay đổi, Adapter có thể thông báo cho RecyclerView qua notifyDataSetChanged(), notifyItemInserted() và các phương thức tương tự. Vấn đề là notifyDataSetChanged() vẽ lại tất cả các mục hiển thị, gây ra hiện tượng giật (jank).
// Adapter với DiffUtil để cập nhật hiệu quả
class ArticleAdapter : RecyclerView.Adapter<ArticleViewHolder>() {
private var articles: List<Article> = emptyList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_article, parent, false)
return ArticleViewHolder(view)
}
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
holder.bind(articles[position])
}
override fun getItemCount(): Int = articles.size
// Cập nhật danh sách qua DiffUtil
fun updateArticles(newArticles: List<Article>) {
val diffCallback = ArticleDiffCallback(articles, newArticles)
val diffResult = DiffUtil.calculateDiff(diffCallback)
articles = newArticles
diffResult.dispatchUpdatesTo(this)
}
}
// Callback cho DiffUtil
class ArticleDiffCallback(
private val oldList: List<Article>,
private val newList: List<Article>
) : DiffUtil.Callback() {
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
return oldList[oldPos].title == newList[newPos].title
}
override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
return oldList[oldPos] == newList[newPos]
}
}
DiffUtil sử dụng thuật toán khác biệt của Eugene W. Myers, tính toán số lượng thao tác tối thiểu (chèn, xóa, di chuyển, thay đổi) để biến đổi danh sách cũ thành danh sách mới. Đối với danh sách 100 mục, DiffUtil hoạt động trong 1–5 ms (Google, 2025). Đối với danh sách lớn (10000+), hãy sử dụng AsyncListDiffer hoặc PagingDataAdapter từ thư viện Paging — chúng thực hiện tính toán trên luồng nền.
ListView là tiền thân của RecyclerView, được giới thiệu trong Android 1.0 (API 1). Nó cũng hiển thị danh sách nhưng không bắt buộc ViewHolder, không hỗ trợ LayoutManager và không thể tạo hoạt ảnh cho các thay đổi. RecyclerView được thiết kế như sự thay thế cho ListView với kiến trúc chặt chẽ hơn và hiệu suất tốt hơn.
| Tiêu chí | RecyclerView | ListView |
|---|---|---|
| ViewHolder | Bắt buộc (kiến trúc) | Khuyến nghị nhưng không bắt buộc |
| LayoutManager | Phân tách: danh sách, lưới, so le | Chỉ danh sách dọc |
| Hoạt ảnh thay đổi | DefaultItemAnimator tích hợp | Không hỗ trợ tích hợp |
| Thông báo thay đổi | Chi tiết: notifyItemInserted/Removed | Chỉ notifyDataSetChanged() cho toàn bộ danh sách |
| DiffUtil | Hỗ trợ qua DiffUtil.Callback | Không có tương đương |
| Dải phân cách | ItemDecoration (tùy chỉnh) | android:divider (tích hợp) |
| Phiên bản tối thiểu | API 14 (Android 4.0) qua AndroidX | API 1 (tất cả phiên bản) |
Khuyến nghị của Google (Android Developers, 2026): sử dụng RecyclerView cho tất cả dự án mới. ListView có thể được giữ lại trong các cơ sở mã cũ nơi việc di chuyển không biện minh cho chi phí. RecyclerView cung cấp hiệu suất tốt hơn với 100+ mục, hỗ trợ cuộn ngang và lưới mà không cần giải pháp thay thế. Trong Android 15, ListView chính thức được đánh dấu là đã lỗi thời một phần.
Câu hỏi thường gặp
Kiểm tra ba điều kiện: (1) Adapter được thiết lập qua recyclerView.adapter = adapter, (2) LayoutManager được thiết lập qua recyclerView.layoutManager = LinearLayoutManager(context), (3) danh sách dữ liệu không trống và adapter đã được thông báo (notifyDataSetChanged()). Một lỗi điển hình là thiết lập dữ liệu sau khi gọi notifyDataSetChanged(). Dữ liệu phải được gán trước khi thông báo. Kiểm tra kích thước danh sách: adapter.itemCount phải lớn hơn 0.
Chậm khi cuộn (jank) xảy ra khi onBindViewHolder() thực hiện các thao tác nặng: tải hình ảnh không có bộ nhớ đệm, findViewById() bên trong bind, tính toán phức tạp. Giải pháp: (1) sử dụng Glide hoặc Coil để tải hình ảnh không đồng bộ, (2) di chuyển tất cả lời gọi findViewById() vào hàm tạo ViewHolder, (3) cho tính toán khác biệt, sử dụng AsyncListDiffer. Bật công cụ lập hồ sơ GPU trong Android Studio (Profile GPU Rendering) để tìm các khung hình chậm.
Truyền hướng cho LayoutManager: LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false). Tham số thứ hai là reverseLayout (false = trái sang phải). Đối với lưới ngang, sử dụng GridLayoutManager với hướng ngang hoặc LinearSnapHelper tùy chỉnh cho cuộn trang. Danh sách ngang phù hợp với băng chuyền sản phẩm, hình ảnh, danh mục.
RecyclerView không có onClickListener tích hợp (không giống ListView với onItemClickListener). Triển khai nó trong ViewHolder: truyền lambda vào hàm tạo và thiết lập itemView.setOnClickListener. Ví dụ: class ArticleViewHolder(itemView: View, val onItemClick: (Article) -> Unit) : RecyclerView.ViewHolder(itemView). Gọi onItemClick(article) bên trong bind(). Thay thế là sử dụng RecyclerView.addOnItemTouchListener() để xử lý cử chỉ.
RecycledViewPool là nhóm nội bộ các ViewHolder đã trở nên vô hình khi cuộn. Thay vì bị hủy, chúng được giữ lại trong nhóm và tái sử dụng. Theo mặc định, nhóm lưu trữ 5 ViewHolder cho mỗi loại. Tăng kích thước cho bố cục phức tạp: recyclerView.getRecycledViewPool().setMaxRecycledViews(VIEW_TYPE_NORMAL, maxCount). Nếu RecyclerView có nhiều loại View (viewType), nhóm duy trì các ngăn xếp riêng biệt cho mỗi loại. Chia sẻ RecycledViewPool giữa các RecyclerView khác nhau giúp tiết kiệm bộ nhớ trên các màn hình có danh sách lồng nhau.
Tổng kế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