In Step 5 you learned how Fragments let you build modular UIs. But as soon as your app needs to display a list of items — a feed, a contact list, a product catalogue — you need something purpose-built for that. Enter RecyclerView.
RecyclerView is the modern replacement for the deprecated ListView. It's faster, more flexible, and handles large datasets efficiently by reusing (recycling) item views as they scroll off screen. This step covers everything from the basics to a production-ready implementation with click listeners, DiffUtil, and multiple layout managers.
RecyclerView — reuses views as they scroll off screen for maximum performance
What Is RecyclerView and Why Use It?
The name tells the story — RecyclerView recycles item views. When an item scrolls off screen, its View isn't discarded. It gets handed to the next incoming item and rebound with new data. This keeps memory flat regardless of how many items are in your list.
| RecyclerView | ListView | |
|---|---|---|
| ViewHolder pattern | ✅ Required — enforced by API | ⚠️ Optional — often skipped, causes jank |
| Efficient updates | ✅ DiffUtil — only redraws changed items | ❌ Full redraw on every data change |
| Layout flexibility | ✅ Linear, Grid, Staggered Grid | ❌ Vertical linear only |
| Animations | ✅ Built-in item add/remove animations | ❌ Manual implementation |
| Status | ✅ Actively maintained Jetpack library | ❌ Deprecated — do not use in new code |
1. The Three Components You Must Know
RecyclerView is built on three collaborating pieces. Understand their roles before writing any code:
- RecyclerView — the scrollable container you place in your layout XML. It manages scrolling and requests views from the Adapter when needed.
- RecyclerView.Adapter — the bridge between your data and the UI. It creates ViewHolders and binds data to them at the right moment.
- RecyclerView.ViewHolder — caches references to views for a single list item. Without it, Android calls
findViewById()on every scroll event — which is slow.
2. Add Dependencies and Enable ViewBinding
Add RecyclerView to build.gradle (app) and enable ViewBinding — it replaces findViewById with null-safe, type-safe generated code:
android {
buildFeatures {
viewBinding true
}
}
dependencies {
implementation 'androidx.recyclerview:recyclerview:1.3.2'
}
3. Create the Layouts
Add RecyclerView to activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
android:clipToPadding="false"/>
</FrameLayout>
Create item_user.xml — the layout for each row in the list:
<?xml version="1.0" encoding="utf-8"?>
<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="16dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/ivAvatar"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_person"
android:scaleType="centerCrop"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="16dp">
<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"/>
<TextView
android:id="@+id/tvEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="13sp"
android:layout_marginTop="2dp"/>
</LinearLayout>
</LinearLayout>
4. Create the Data Model
// User.kt
data class User(
val id: Int, // Unique identifier — used by DiffUtil
val name: String,
val email: String
)
5. Build the Adapter with ListAdapter and DiffUtil
Use ListAdapter instead of the base RecyclerView.Adapter. It has DiffUtil built in — it calculates the difference between old and new lists on a background thread and only redraws items that actually changed. The result is smooth, flicker-free updates even with large lists.
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
class UserAdapter(
private val onItemClick: (User) -> Unit
) : ListAdapter<User, UserAdapter.ViewHolder>(UserDiffCallback()) {
inner class ViewHolder(
private val binding: ItemUserBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(user: User) {
binding.tvName.text = user.name
binding.tvEmail.text = user.email
// Set click listener here in bind() — not in onCreateViewHolder
// onCreateViewHolder captures a stale position
binding.root.setOnClickListener { onItemClick(user) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemUserBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
}
}
// DiffUtil — tells RecyclerView exactly which items changed
class UserDiffCallback : DiffUtil.ItemCallback<User>() {
// Same item = same unique ID (not the same content)
override fun areItemsTheSame(oldItem: User, newItem: User) =
oldItem.id == newItem.id
// Same content = no visual update needed
override fun areContentsTheSame(oldItem: User, newItem: User) =
oldItem == newItem // data class == compares all fields
}
6. Set Up RecyclerView in Activity
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var adapter: UserAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupRecyclerView()
loadData()
}
private fun setupRecyclerView() {
adapter = UserAdapter { user ->
Toast.makeText(this, "Clicked: ${user.name}", Toast.LENGTH_SHORT).show()
}
binding.recyclerView.apply {
layoutManager = LinearLayoutManager(this@MainActivity)
this.adapter = this@MainActivity.adapter
setHasFixedSize(true) // Free performance win — size doesn't change with content
}
}
private fun loadData() {
val users = listOf(
User(1, "Alice Johnson", "alice@example.com"),
User(2, "Bob Smith", "bob@example.com"),
User(3, "Carol White", "carol@example.com"),
User(4, "David Brown", "david@example.com"),
User(5, "Eve Davis", "eve@example.com"),
)
adapter.submitList(users) // DiffUtil handles the diff automatically
}
}
7. Switching Layouts — Linear, Grid, and Staggered Grid
Swap the LayoutManager to change how your list renders — the Adapter stays exactly the same:
// Vertical list (default)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
// Horizontal scrolling list (like a carousel)
binding.recyclerView.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
// 2-column grid
binding.recyclerView.layoutManager = GridLayoutManager(this, 2)
// Staggered grid — items can be different heights (Pinterest-style)
binding.recyclerView.layoutManager =
StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
8. Item Spacing with ItemDecoration
Never add margin to item layouts for spacing — it applies inconsistently to first/last items. Use ItemDecoration instead:
// Built-in divider line between items
binding.recyclerView.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)
// Custom equal spacing between all items
class SpaceItemDecoration(private val space: Int) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect, view: View,
parent: RecyclerView, state: RecyclerView.State
) {
outRect.bottom = space
// Add top spacing only for the first item if needed
if (parent.getChildAdapterPosition(view) == 0) {
outRect.top = space
}
}
}
// Usage
binding.recyclerView.addItemDecoration(
SpaceItemDecoration(resources.getDimensionPixelSize(R.dimen.item_space))
)
Best Practices
- Use ListAdapter + DiffUtil — not notifyDataSetChanged() —
notifyDataSetChanged()redraws the entire list every time, loses scroll position, and kills animations.ListAdapter+DiffUtilonly touches what changed. The visual difference is immediately obvious. - Use ViewBinding over
findViewById— null-safe, type-safe, zero runtime overhead. Enable it once in Gradle and it generates binding classes for every layout automatically. - Pass click listeners as lambdas in the constructor — never set click listeners in
onCreateViewHolder. It captures a stale adapter position. Always set them insidebind(). - Keep ViewHolder logic minimal — the ViewHolder should only set text and images. Business logic, data fetching, and state management belong in a ViewModel.
- Use
setHasFixedSize(true)when possible — if adding or removing items doesn't change the RecyclerView's own size in the layout, this is a free performance improvement.
Try It Yourself
- Add a delete action — add a delete button to each item view. When clicked, remove that user from the list and call
adapter.submitList()with the updated list. Observe how DiffUtil animates only the removed item. - Switch to a grid — change the
LayoutManagertoGridLayoutManager(this, 2)and update your item layout to look good in a 2-column grid. Run the app — the Adapter needs zero changes. - Add a search filter — add an
EditTextabove the RecyclerView. As the user types, filter the list by name and calladapter.submitList(filteredList). Watch DiffUtil animate the items in and out.
Your app now displays efficient scrollable lists. But what happens to your list data when the user rotates the screen? In Step 7: LiveData and ViewModel — Smarter Data Handling we learn how to keep UI data intact across the entire Android lifecycle.
Frequently Asked Questions
What is the difference between RecyclerView.Adapter and ListAdapter?
RecyclerView.Adapter is the base class — you manage the list manually and trigger a full redraw with notifyDataSetChanged(). ListAdapter has DiffUtil built in, runs the diff calculation on a background thread, and only redraws items that actually changed. Always use ListAdapter.
Why does my RecyclerView flicker when I update data?
You're calling notifyDataSetChanged(), which redraws the entire list and loses scroll position. Switch to ListAdapter + DiffUtil and call adapter.submitList(newList) — only changed items are redrawn, with smooth animations.
How do I make a RecyclerView scroll horizontally?
Set LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) as the LayoutManager. Your Adapter doesn't need any changes — LayoutManager and Adapter are completely independent.
Can I have multiple view types in one RecyclerView?
Yes — override getItemViewType(), inflate the correct layout in onCreateViewHolder based on the type integer, and bind correctly in onBindViewHolder. See our RecyclerView Multiple View Types guide for the full implementation with sealed classes.
- ← Step 5: Fragments — Modular and Flexible UI Design
- Series Hub: Learn Android with Kotlin — Full Roadmap
- Step 7: LiveData and ViewModel — Smarter Data Handling →
- Related: RecyclerView with ItemClickListener in Kotlin
- Related: RecyclerView with Multiple View Types in Kotlin
- Related: RecyclerView with Endless Scrolling — Pagination
- RecyclerView recycles views — memory stays flat regardless of list size
- Three parts: RecyclerView (container) + Adapter (data bridge) + ViewHolder (view cache)
- Use ListAdapter + DiffUtil — only redraws changed items, smooth animations, no flicker
- Use ViewBinding in ViewHolder — null-safe, no
findViewById() - Set click listeners inside
bind()— never inonCreateViewHolder() - Swap LayoutManager to switch between linear, grid, and staggered layouts
- Use ItemDecoration for spacing — never add margins to item layouts
setHasFixedSize(true)— free performance improvement when list size is stable