RecyclerView & Lists

Introduction
RecyclerView is the modern, flexible, and efficient replacement for ListView and GridView in Android. It's designed to display large datasets efficiently by recycling views and providing a highly customizable framework for creating complex list and grid layouts.
This comprehensive guide will take you from basic RecyclerView implementation to advanced features like multiple view types, item animations, and custom layouts. Whether you're building a simple contact list or a complex social media feed, RecyclerView provides the tools you need.
Why RecyclerView?
Before diving into implementation, let's understand why RecyclerView is the preferred choice for displaying lists in Android:
- View Recycling: Efficiently reuses views to handle large datasets
- Flexible Layouts: Supports linear, grid, and staggered grid layouts
- Item Animations: Built-in support for smooth animations
- Multiple View Types: Display different item layouts in the same list
- Performance: Optimized for smooth scrolling and memory efficiency
- Customization: Highly customizable for complex UI requirements
Basic RecyclerView Setup
Let's start with a simple RecyclerView implementation to understand the basic components.
Layout Setup
Data Class
data class Item(
val id: Int,
val title: String,
val description: String,
val iconResId: Int
)
Basic Implementation
class MainActivity : AppCompatActivity() {
private lateinit var recyclerView: RecyclerView
private lateinit var adapter: ItemAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView = findViewById(R.id.recycler_view)
// Set layout manager
recyclerView.layoutManager = LinearLayoutManager(this)
// Create sample data
val items = listOf(
Item(1, "Item 1", "Description for item 1", R.drawable.ic_item),
Item(2, "Item 2", "Description for item 2", R.drawable.ic_item),
Item(3, "Item 3", "Description for item 3", R.drawable.ic_item)
)
// Set adapter
adapter = ItemAdapter(items)
recyclerView.adapter = adapter
}
}
Adapter and ViewHolder Pattern
The adapter pattern is the core of RecyclerView. It bridges the gap between your data and the UI.
Basic Adapter Implementation
class ItemAdapter(private val items: List- ) :
RecyclerView.Adapter
() {
class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val icon: ImageView = itemView.findViewById(R.id.item_icon)
private val title: TextView = itemView.findViewById(R.id.item_title)
private val description: TextView = itemView.findViewById(R.id.item_description)
fun bind(item: Item) {
icon.setImageResource(item.iconResId)
title.text = item.title
description.text = item.description
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_layout, parent, false)
return ItemViewHolder(view)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount(): Int = items.size
}
Enhanced Adapter with Click Handling
class ItemAdapter(
private val items: List- ,
private val onItemClick: (Item) -> Unit
) : RecyclerView.Adapter
() {
class ItemViewHolder(
itemView: View,
private val onItemClick: (Item) -> Unit
) : RecyclerView.ViewHolder(itemView) {
private val icon: ImageView = itemView.findViewById(R.id.item_icon)
private val title: TextView = itemView.findViewById(R.id.item_title)
private val description: TextView = itemView.findViewById(R.id.item_description)
fun bind(item: Item) {
icon.setImageResource(item.iconResId)
title.text = item.title
description.text = item.description
itemView.setOnClickListener {
onItemClick(item)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_layout, parent, false)
return ItemViewHolder(view, onItemClick)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount(): Int = items.size
}
Layout Managers
Layout managers control how items are positioned and displayed in the RecyclerView.
LinearLayoutManager
// Vertical list (default)
recyclerView.layoutManager = LinearLayoutManager(this)
// Horizontal list
recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
// Reverse order
recyclerView.layoutManager = LinearLayoutManager(this).apply {
reverseLayout = true
stackFromEnd = true
}
GridLayoutManager
// 2-column grid
recyclerView.layoutManager = GridLayoutManager(this, 2)
// 3-column grid with different span sizes
val gridLayoutManager = GridLayoutManager(this, 3)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (position) {
0 -> 3 // First item spans all 3 columns
else -> 1 // Other items span 1 column
}
}
}
recyclerView.layoutManager = gridLayoutManager
StaggeredGridLayoutManager
// Staggered grid (like Pinterest)
recyclerView.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
// Horizontal staggered grid
recyclerView.layoutManager = StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.HORIZONTAL)
Multiple View Types
RecyclerView supports displaying different item layouts in the same list, which is useful for headers, footers, and different content types.
Defining View Types
sealed class ListItem {
data class Header(val title: String) : ListItem()
data class Content(val item: Item) : ListItem()
data class Footer(val text: String) : ListItem()
}
class MultiTypeAdapter(private val items: List) :
RecyclerView.Adapter() {
companion object {
private const val VIEW_TYPE_HEADER = 0
private const val VIEW_TYPE_CONTENT = 1
private const val VIEW_TYPE_FOOTER = 2
}
override fun getItemViewType(position: Int): Int {
return when (items[position]) {
is ListItem.Header -> VIEW_TYPE_HEADER
is ListItem.Content -> VIEW_TYPE_CONTENT
is ListItem.Footer -> VIEW_TYPE_FOOTER
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
VIEW_TYPE_HEADER -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_header, parent, false)
HeaderViewHolder(view)
}
VIEW_TYPE_CONTENT -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_content, parent, false)
ContentViewHolder(view)
}
VIEW_TYPE_FOOTER -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_footer, parent, false)
FooterViewHolder(view)
}
else -> throw IllegalArgumentException("Invalid view type")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = items[position]) {
is ListItem.Header -> (holder as HeaderViewHolder).bind(item)
is ListItem.Content -> (holder as ContentViewHolder).bind(item)
is ListItem.Footer -> (holder as FooterViewHolder).bind(item)
}
}
override fun getItemCount(): Int = items.size
}
ViewHolder Classes
class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val titleText: TextView = itemView.findViewById(R.id.header_title)
fun bind(header: ListItem.Header) {
titleText.text = header.title
}
}
class ContentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val icon: ImageView = itemView.findViewById(R.id.item_icon)
private val title: TextView = itemView.findViewById(R.id.item_title)
private val description: TextView = itemView.findViewById(R.id.item_description)
fun bind(content: ListItem.Content) {
icon.setImageResource(content.item.iconResId)
title.text = content.item.title
description.text = content.item.description
}
}
class FooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val footerText: TextView = itemView.findViewById(R.id.footer_text)
fun bind(footer: ListItem.Footer) {
footerText.text = footer.text
}
}
Item Animations
Animations make your RecyclerView feel more dynamic and engaging. Android provides built-in animations and allows for custom implementations.
Default Animations
// Use default animations
recyclerView.itemAnimator = DefaultItemAnimator()
// Disable animations
recyclerView.itemAnimator = null
// Custom animation duration
val animator = DefaultItemAnimator()
animator.addDuration = 300
animator.removeDuration = 300
animator.moveDuration = 300
animator.changeDuration = 300
recyclerView.itemAnimator = animator
Custom Item Animations
class CustomItemAnimator : DefaultItemAnimator() {
override fun animateAdd(holder: RecyclerView.ViewHolder): Boolean {
holder.itemView.alpha = 0f
holder.itemView.translationY = 100f
holder.itemView.animate()
.alpha(1f)
.translationY(0f)
.setDuration(300)
.setInterpolator(FastOutSlowInInterpolator())
.start()
return true
}
override fun animateRemove(holder: RecyclerView.ViewHolder): Boolean {
holder.itemView.animate()
.alpha(0f)
.translationX(holder.itemView.width.toFloat())
.setDuration(300)
.setInterpolator(FastOutSlowInInterpolator())
.withEndAction {
holder.itemView.alpha = 1f
holder.itemView.translationX = 0f
}
.start()
return true
}
}
// Apply custom animator
recyclerView.itemAnimator = CustomItemAnimator()
Item Decorations
Item decorations allow you to add visual elements like dividers, spacing, and backgrounds to your RecyclerView items.
Divider Decoration
class DividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
private val divider = ContextCompat.getDrawable(context, R.drawable.divider)
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val left = parent.paddingLeft
val right = parent.width - parent.paddingRight
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
val params = child.layoutParams as RecyclerView.LayoutParams
val top = child.bottom + params.bottomMargin
val bottom = top + (divider?.intrinsicHeight ?: 0)
divider?.setBounds(left, top, right, bottom)
divider?.draw(c)
}
}
}
// Apply divider decoration
recyclerView.addItemDecoration(DividerItemDecoration(this))
Spacing Decoration
class SpacingItemDecoration(private val spacing: Int) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
outRect.left = spacing
outRect.right = spacing
outRect.top = spacing
outRect.bottom = spacing
}
}
// Apply spacing decoration
recyclerView.addItemDecoration(SpacingItemDecoration(16))
Advanced Features
RecyclerView provides several advanced features for complex use cases.
Drag and Drop
class DragDropAdapter(
private val items: MutableList- ,
private val onItemClick: (Item) -> Unit
) : RecyclerView.Adapter
() {
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
// ViewHolder implementation
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_layout, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// Binding implementation
}
override fun getItemCount(): Int = items.size
fun moveItem(fromPosition: Int, toPosition: Int) {
if (fromPosition < toPosition) {
for (i in fromPosition until toPosition) {
Collections.swap(items, i, i + 1)
}
} else {
for (i in fromPosition downTo toPosition + 1) {
Collections.swap(items, i, i - 1)
}
}
notifyItemMoved(fromPosition, toPosition)
}
}
// Setup drag and drop
val callback = object : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val fromPosition = viewHolder.adapterPosition
val toPosition = target.adapterPosition
(recyclerView.adapter as DragDropAdapter).moveItem(fromPosition, toPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// Handle swipe
}
}
ItemTouchHelper(callback).attachToRecyclerView(recyclerView)
Swipe to Delete
val swipeCallback = object : ItemTouchHelper.SimpleCallback(
0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition
// Remove item from data source
items.removeAt(position)
adapter.notifyItemRemoved(position)
// Show undo option
showUndoSnackbar(position)
}
}
ItemTouchHelper(swipeCallback).attachToRecyclerView(recyclerView)
Performance Optimization
Optimizing RecyclerView performance is crucial for smooth user experience, especially with large datasets.
ViewHolder Pattern Best Practices
- Cache view references: Store findViewById results in ViewHolder
- Minimize object creation: Reuse ViewHolder instances
- Use efficient binding: Only update changed views
Data Binding Integration
class DataBindingAdapter(private val items: List- ) :
RecyclerView.Adapter
() {
class ViewHolder(private val binding: ItemLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Item) {
binding.item = item
binding.executePendingBindings()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemLayoutBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount(): Int = items.size
}
DiffUtil for Efficient Updates
class DiffUtilAdapter : ListAdapter- (ItemDiffCallback()) {
class ItemDiffCallback : DiffUtil.ItemCallback
- () {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem == newItem
}
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
// ViewHolder implementation
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
// Implementation
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// Implementation
}
}
// Update data efficiently
val newItems = getUpdatedItems()
adapter.submitList(newItems)
Practical Examples
Let's look at some real-world RecyclerView implementations.
Contact List App
data class Contact(
val id: Long,
val name: String,
val phone: String,
val email: String,
val avatar: String?
)
class ContactAdapter(
private val contacts: List,
private val onContactClick: (Contact) -> Unit
) : RecyclerView.Adapter() {
class ContactViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val avatar: ImageView = itemView.findViewById(R.id.contact_avatar)
private val name: TextView = itemView.findViewById(R.id.contact_name)
private val phone: TextView = itemView.findViewById(R.id.contact_phone)
fun bind(contact: Contact, onContactClick: (Contact) -> Unit) {
name.text = contact.name
phone.text = contact.phone
// Load avatar with Glide or similar library
Glide.with(itemView.context)
.load(contact.avatar)
.placeholder(R.drawable.default_avatar)
.into(avatar)
itemView.setOnClickListener { onContactClick(contact) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContactViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_contact, parent, false)
return ContactViewHolder(view)
}
override fun onBindViewHolder(holder: ContactViewHolder, position: Int) {
holder.bind(contacts[position], onContactClick)
}
override fun getItemCount(): Int = contacts.size
}
Social Media Feed
sealed class FeedItem {
data class Post(
val id: String,
val author: String,
val content: String,
val imageUrl: String?,
val likes: Int,
val comments: Int
) : FeedItem()
data class Story(val id: String, val author: String, val imageUrl: String) : FeedItem()
data class Ad(val id: String, val title: String, val imageUrl: String) : FeedItem()
}
class FeedAdapter(private val items: List) :
RecyclerView.Adapter() {
companion object {
private const val VIEW_TYPE_POST = 0
private const val VIEW_TYPE_STORY = 1
private const val VIEW_TYPE_AD = 2
}
override fun getItemViewType(position: Int): Int {
return when (items[position]) {
is FeedItem.Post -> VIEW_TYPE_POST
is FeedItem.Story -> VIEW_TYPE_STORY
is FeedItem.Ad -> VIEW_TYPE_AD
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
VIEW_TYPE_POST -> PostViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_post, parent, false)
)
VIEW_TYPE_STORY -> StoryViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_story, parent, false)
)
VIEW_TYPE_AD -> AdViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_ad, parent, false)
)
else -> throw IllegalArgumentException("Invalid view type")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = items[position]) {
is FeedItem.Post -> (holder as PostViewHolder).bind(item)
is FeedItem.Story -> (holder as StoryViewHolder).bind(item)
is FeedItem.Ad -> (holder as AdViewHolder).bind(item)
}
}
override fun getItemCount(): Int = items.size
}
Best Practices
Adapter Design
- Keep adapters simple: Focus on data binding, not business logic
- Use efficient data structures: Choose appropriate collections
- Implement proper equals/hashCode: For DiffUtil efficiency
- Handle empty states: Show appropriate UI when list is empty
Performance
- Use DiffUtil: For efficient list updates
- Optimize view inflation: Use ViewStub for conditional layouts
- Minimize overdraw: Use efficient backgrounds
- Profile your app: Use Android Studio profiler
User Experience
- Add loading states: Show progress indicators
- Implement pull-to-refresh: Use SwipeRefreshLayout
- Add item animations: Make interactions feel smooth
- Handle errors gracefully: Show retry options
Common Pitfalls
Avoiding Common Mistakes
- Don't modify data in onBindViewHolder: Keep binding pure
- Don't forget to call notifyDataSetChanged: When data changes
- Don't use ListView patterns: RecyclerView is different
- Don't ignore view recycling: Always reset view state
Debugging Tips
- Use Layout Inspector: Debug layout issues
- Enable layout bounds: See view boundaries
- Check adapter position: Ensure valid positions
- Monitor memory usage: Watch for memory leaks
Practice Exercises
Try these exercises to reinforce your RecyclerView knowledge:
Exercise 1: Todo List App
// Create a todo list with:
// - Add/remove todo items
// - Mark items as complete
// - Drag and drop reordering
// - Swipe to delete
// - Different view types for completed vs pending
Exercise 2: Photo Gallery
// Build a photo gallery with:
// - Grid layout for photos
// - Staggered grid for different photo sizes
// - Image loading with placeholders
// - Click to view full screen
// - Pull to refresh
Exercise 3: Chat Interface
// Create a chat interface with:
// - Different message types (text, image, system)
// - Timestamp headers
// - Message status indicators
// - Smooth scrolling to bottom
// - Typing indicators
Next Steps
Now that you have a solid foundation in RecyclerView, explore these advanced topics:
- Paging 3: Load large datasets efficiently
- ConcatAdapter: Combine multiple adapters
- ListAdapter: Built-in DiffUtil support
- Custom Layout Managers: Create unique layouts
- RecyclerView with Compose: Modern UI toolkit integration
- Advanced Animations: Complex transition effects
Resources
Summary
RecyclerView is a powerful and flexible component that forms the foundation of modern Android list and grid interfaces. Its efficient view recycling, customizable layouts, and rich feature set make it the go-to choice for displaying dynamic content.
You've learned about adapters, view holders, layout managers, animations, and advanced features like drag-and-drop and multiple view types. Remember to always consider performance, user experience, and maintainability when implementing RecyclerView in your apps.
Practice regularly with different use cases, experiment with custom implementations, and stay updated with the latest Android development patterns. The more you work with RecyclerView, the more you'll appreciate its flexibility and power.