RecyclerView is an Android Jetpack component for efficiently displaying large data sets as lists and grids. Unlike ListView, RecyclerView reuses ViewHolders and manages layout through LayoutManager. According to Google (Android Developers, 2026), RecyclerView processes up to 60 frames per second when scrolling a list of 1000+ items thanks to ViewHolder pooling and the DiffUtil diff algorithm. Learn more about Android app architecture in our article about ListView.
Key Takeaways
RecyclerView is a component of the Android Jetpack library, introduced at Google I/O 2014 as part of the Android Support Library v7. It is designed for displaying dynamic lists and grids with high performance. RecyclerView emerged as a replacement for ListView and solves its predecessor's key issue — the lack of enforced ViewHolder reuse.
The RecyclerView architecture is built on the Model-View-Adapter pattern: data is stored in a model (List, LiveData, Flow), rendering is managed by the Adapter, and layout is controlled by the LayoutManager. Each component is responsible for its own area, making the system flexible and extensible.
According to an Android Developers survey (2025), RecyclerView is used in 78% of Android apps in the Google Play top 100. Scrolling performance for a list of 1000+ items remains stable at 60 FPS on mid-range devices thanks to three mechanisms: ViewHolder pooling, diff-based update calculation, and asynchronous data loading.
RecyclerView is optimal for: lists with an unknown number of items (feeds, chats, logs), N-column grids (galleries, catalogs), staggered layouts (Pinterest boards), and lists with add/remove animations. For simple screens with 2–5 items, LinearLayout or ScrollView are simpler and more efficient — RecyclerView adds overhead.
To use RecyclerView, add the dependency to build.gradle (app): implementation 'androidx.recyclerview:recyclerview:1.4.0'. Minimum API version is 14 (Android 4.0). Starting with AndroidX RecyclerView 1.3.0, modules recyclerview-selection for item selection and recyclerview-swipe for swipe gestures are available.
RecyclerView is divided into three independent layers: the Adapter manages data and ViewHolders, the LayoutManager handles item positioning, and the ItemAnimator controls animations. This architecture allows replacing any layer without modifying the others — for example, switching from LinearLayoutManager to GridLayoutManager without changing the Adapter code.
| Component | Responsibilities | Implementation |
|---|---|---|
| Adapter | Creating ViewHolders, binding data to View, notifying about changes | RecyclerView.Adapter<VH> |
| ViewHolder | Caching View references, avoiding findViewById() | RecyclerView.ViewHolder |
| LayoutManager | Item positioning, determining scroll direction | LinearLayoutManager, GridLayoutManager, StaggeredGridLayoutManager |
| ItemAnimator | Animation for adding, removing, moving items | DefaultItemAnimator |
| ItemDecoration | Drawing dividers, spacing, backgrounds between items | RecyclerView.ItemDecoration |
Work cycle: (1) Adapter creates a ViewHolder via onCreateViewHolder(), (2) LayoutManager determines the position of the new View on screen, (3) Adapter binds data via onBindViewHolder(), (4) during scrolling, ViewHolders that go off-screen enter the pool (RecycledViewPool) for reuse. Instead of creating new Views, existing ones are reused — this is the key performance mechanism.
ViewHolder is a class that stores references to Views (TextView, ImageView, etc.) inside a list item. Without ViewHolder, each scroll would trigger findViewById() — one of the most expensive Android UI operations. ViewHolder eliminates this call by caching references after the initial binding.
// Data model
data class Article(
title: String,
summary: String,
author: String,
imageUrl: String?
)
// ViewHolder — stores references to 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
}
}
Practical rule: every View in the item layout should be declared as a ViewHolder property with findViewById() exactly once — in the constructor. The bind() method does not call findViewById(), it only sets text, colors, and listeners. This approach speeds up list scrolling by 30–50% according to Google (Android Performance Patterns, 2025).
LayoutManager defines how items are positioned inside RecyclerView. Android provides three default implementations: LinearLayoutManager, GridLayoutManager, and StaggeredGridLayoutManager. The choice of LayoutManager affects data perception and scrolling performance.
| LayoutManager | Arrangement | When to use | Setup method |
|---|---|---|---|
| LinearLayoutManager | List (vertical or horizontal) | Feeds, chats, notifications, product lists | LinearLayoutManager(context) |
| GridLayoutManager | N-column grid | Galleries, catalogs, icons, image grids | GridLayoutManager(context, spanCount) |
| StaggeredGridLayoutManager | Staggered grid (different item heights) | Pinterest boards, notes, masonry galleries | StaggeredGridLayoutManager(spanCount, orientation) |
// Setting up LinearLayoutManager in 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)
// Optimization: fixed size + pool
recyclerView.setHasFixedSize(true)
recyclerView.setItemViewCacheSize(20)
// Divider between items
val divider = DividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL)
recyclerView.addItemDecoration(divider)
}
}
Performance: LinearLayoutManager with vertical scrolling is the fastest option because items are laid out sequentially and the LayoutManager computes positions in O(1). GridLayoutManager requires additional calculations for column splitting but remains performant with a fixed spanCount. StaggeredGridLayoutManager is the slowest of the three as it needs to calculate item positions in a cascade considering neighboring item heights. Use it only when visually different cell heights are required.
Adapter in RecyclerView implements the GoF Adapter pattern: it transforms data (List<T>) into ViewHolders displayed on screen. When data changes, the Adapter can notify RecyclerView via notifyDataSetChanged(), notifyItemInserted(), and similar methods. The problem is that notifyDataSetChanged() redraws all visible items, causing jank.
// Adapter with DiffUtil for efficient updates
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
// Updating list via DiffUtil
fun updateArticles(newArticles: List<Article>) {
val diffCallback = ArticleDiffCallback(articles, newArticles)
val diffResult = DiffUtil.calculateDiff(diffCallback)
articles = newArticles
diffResult.dispatchUpdatesTo(this)
}
}
// Callback for 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 uses the Eugene W. Myers' difference algorithm, which computes the minimum number of operations (insert, remove, move, change) to transform the old list into the new one. For a list of 100 items, DiffUtil works in 1–5 ms (Google, 2025). For large lists (10000+), use AsyncListDiffer or PagingDataAdapter from the Paging library — they perform calculations on a background thread.
ListView is the predecessor of RecyclerView, introduced in Android 1.0 (API 1). It also displays lists but does not enforce ViewHolder, does not support LayoutManager, and cannot animate changes. RecyclerView was designed as a replacement for ListView with a stricter architecture and better performance.
| Criterion | RecyclerView | ListView |
|---|---|---|
| ViewHolder | Mandatory (architecture) | Recommended but not required |
| LayoutManager | Separation: list, grid, staggered | Vertical list only |
| Change animation | DefaultItemAnimator out of the box | No built-in support |
| Change notification | Granular: notifyItemInserted/Removed | notifyDataSetChanged() only for the entire list |
| DiffUtil | Support via DiffUtil.Callback | No equivalent |
| Dividers | ItemDecoration (custom) | android:divider (built-in) |
| Minimum version | API 14 (Android 4.0) via AndroidX | API 1 (all versions) |
Google recommendation (Android Developers, 2026): use RecyclerView for all new projects. ListView can remain in legacy codebases where migration does not justify the cost. RecyclerView offers better performance on 100+ items, supports horizontal scrolling and grids without additional workarounds. In Android 15, ListView is officially marked as partially deprecated.
Frequently Asked Questions
Check three conditions: (1) Adapter is set via recyclerView.adapter = adapter, (2) LayoutManager is set via recyclerView.layoutManager = LinearLayoutManager(context), (3) the data list is not empty and the adapter has been notified (notifyDataSetChanged()). A typical mistake is setting data after calling notifyDataSetChanged(). Data should be assigned before notification. Check the list size: adapter.itemCount must be greater than 0.
Lag during scrolling (jank) occurs when onBindViewHolder() performs heavy operations: loading images without caching, findViewById() inside bind, complex calculations. Solutions: (1) use Glide or Coil for async image loading, (2) move all findViewById() calls to the ViewHolder constructor, (3) for diff calculations, use AsyncListDiffer. Enable GPU profiling in Android Studio (Profile GPU Rendering) to find slow frames.
Pass the orientation to LayoutManager: LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false). The second parameter is reverseLayout (false = left to right). For a horizontal grid, use GridLayoutManager with horizontal orientation or a custom LinearSnapHelper for paged scrolling. Horizontal lists work well for carousels of products, images, categories.
RecyclerView does not have a built-in onClickListener (unlike ListView with onItemClickListener). Implement it in ViewHolder: pass a lambda to the constructor and set itemView.setOnClickListener. Example: class ArticleViewHolder(itemView: View, val onItemClick: (Article) -> Unit) : RecyclerView.ViewHolder(itemView). Call onItemClick(article) inside bind(). An alternative is to use RecyclerView.addOnItemTouchListener() for gesture handling.
RecycledViewPool is an internal pool of ViewHolders that have become invisible during scrolling. Instead of being destroyed, they are kept in the pool and reused. By default, the pool stores 5 ViewHolders of each type. Increase the size for complex layouts: recyclerView.getRecycledViewPool().setMaxRecycledViews(VIEW_TYPE_NORMAL, maxCount). If a RecyclerView has multiple View types (viewType), the pool keeps separate stacks for each. Sharing a RecycledViewPool between different RecyclerViews saves memory in screens with nested lists.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also