RecyclerView & Lists

Intermediate April 2025 Mohsen Mashkour
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.