Working with RecyclerView
1. Creating RecyclerView Adapter
| Property | Description | Values |
|---|---|---|
| onCreateViewHolder | Creates a new ViewHolder for the RecyclerView | RecyclerView.ViewHolder |
| onBindViewHolder | Binds data to the ViewHolder at the specified position | RecyclerView.ViewHolder, Int |
| getItemCount | Returns the total number of items in the data set | Int |
Example: Basic RecyclerView Adapter
class MyAdapter(private val items: List<String>) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val textView: TextView = view.findViewById(R.id.textView)
}
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) {
holder.textView.text = items[position]
}
override fun getItemCount(): Int = items.size
}
Note: The adapter must extend
RecyclerView.Adapter and implement the required methods.
2. Implementing ViewHolder Pattern
| Component | Description | Usage |
|---|---|---|
| ViewHolder | Encapsulates the view elements of a single item in the list | Extend RecyclerView.ViewHolder |
| onCreateViewHolder | Creates a new ViewHolder instance | Called by RecyclerView to inflate layout |
| onBindViewHolder | Binds data to the ViewHolder's views | Called by RecyclerView to populate item views |
Example: Basic ViewHolder Implementation
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val titleTextView: TextView = itemView.findViewById(R.id.title)
val descriptionTextView: TextView = itemView.findViewById(R.id.description)
}
class MyAdapter(private val items: List<MyItem>) : RecyclerView.Adapter<MyViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_layout, parent, false)
return MyViewHolder(view)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = items[position]
holder.titleTextView.text = item.title
holder.descriptionTextView.text = item.description
}
override fun getItemCount(): Int = items.size
}
Note: The ViewHolder pattern prevents unnecessary view lookups and improves performance by caching view references.
3. Binding Data to Views
| Property | Description | Example |
|---|---|---|
| itemView | Reference to the root view of the item layout | val itemView = findViewById<View>(R.id.item_layout) |
| binding | View binding instance for the item layout | val binding = ItemLayoutBinding.bind(itemView) |
Example: Binding Data to Views in RecyclerView
class MyAdapter(private val dataList: List<DataItem>) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
inner class ViewHolder(val binding: ItemLayoutBinding) : RecyclerView.ViewHolder(binding.root)
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) {
val item = dataList[position]
holder.binding.textView.text = item.title
holder.binding.imageView.setImageResource(item.imageResId)
}
override fun getItemCount(): Int = dataList.size
}
Note: Using view binding improves performance and type safety compared to traditional findViewById calls.
4. Setting Layout Manager
| Property | Description | Values |
|---|---|---|
| layoutManager | Sets the layout manager for the RecyclerView | LinearLayoutManager, GridLayoutManager, StaggeredGridLayoutManager |
Example: Setting LinearLayoutManager
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
Example: Setting GridLayoutManager
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = GridLayoutManager(this, 2)
Example: Setting StaggeredGridLayoutManager
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
5. Handling Item Click Events
| Event Type | Description | Handler Method | Notes |
|---|---|---|---|
| onBindViewHolder | Called when a view holder is bound to data | ViewHolder.itemView.setOnClickListener { ... } |
Most common approach for handling clicks |
| ViewHolder | View holder instance with access to item view | holder.itemView.setOnClickListener { ... } |
Use holder.adapterPosition to get item index |
Example: Click Listener in ViewHolder
class MyAdapter(
private val items: List<String>
) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(item: String) {
itemView.setOnClickListener {
// Handle click event
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
// Perform action with item at position
println("Clicked item: $item at position $position")
}
}
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
// ... other required methods
}
Note: Always check
adapterPosition != RecyclerView.NO_POSITION before using the position to avoid errors.Best Practices for Click Handling
- Use
adapterPositionto get the actual item index - Check for
RecyclerView.NO_POSITIONto prevent errors - Handle click events inside
onBindViewHolderor in the ViewHolder - Consider using
setOnLongClickListenerfor long-press actions
6. Using DiffUtil for Updates
DiffUtil is a utility class in Android that helps calculate the difference between two lists and efficiently update a RecyclerView with minimal performance impact.
Example: Using DiffUtil to Update RecyclerView
class UserDiffCallback(
private val oldList: List<User>,
private val newList: List<User>
) : DiffUtil.Callback() {
override fun getOldListSize(): Int = oldList.size
override fun getNewListSize(): Int = newList.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition].id == newList[newItemPosition].id
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition] == newList[newItemPosition]
}
}
class UserAdapter(private var users: List<User>) : RecyclerView.Adapter<UserViewHolder>() {
fun updateUsers(newUsers: List<User>) {
val diffCallback = UserDiffCallback(users, newUsers)
val diffResult = DiffUtil.calculateDiff(diffCallback)
users = newUsers
diffResult.dispatchUpdatesTo(this)
}
// ... other adapter methods
}
| Method | Description | Notes |
|---|---|---|
| getOldListSize() | Returns the size of the old list | Required override |
| getNewListSize() | Returns the size of the new list | Required override |
| areItemsTheSame() | Checks if items at given positions are the same | Used to identify moved or deleted items |
| areContentsTheSame() | Checks if contents of items at given positions are the same | Used to determine if an item needs to be updated visually |
Note:
DiffUtil is more efficient than calling notifyDataSetChanged() because it calculates only the minimal set of changes needed to update the UI.
Warning: Make sure
areItemsTheSame correctly identifies items by a unique identifier (e.g., ID) to avoid incorrect updates.
7. Implementing ListAdapter
ListAdapter is a powerful utility in Android development that simplifies the process of binding data to a RecyclerView. It automatically handles diffing and updates, making it more efficient than manually managing data changes.
| Property | Description | Notes |
|---|---|---|
| ListAdapter | Base class for adapting data to RecyclerView | Extends Adapter and handles automatic diffing |
| submitList() | Updates the list with new data | Triggers diff calculation and UI update |
| DiffUtil.ItemCallback | Defines how items are compared for changes | Required to calculate differences efficiently |
Example: Basic ListAdapter Implementation
class MyAdapter : ListAdapter<MyItem, MyViewHolder>(MyDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
// Create and return ViewHolder
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(getItem(position))
}
}
class MyDiffCallback : DiffUtil.ItemCallback<MyItem>() {
override fun areItemsTheSame(old: MyItem, new: MyItem): Boolean = old.id == new.id
override fun areContentsTheSame(old: MyItem, new: MyItem): Boolean = old == new
}
Note:
ListAdapter uses DiffUtil under the hood to efficiently calculate changes between lists, reducing unnecessary UI updates.
Warning: Ensure that
DiffUtil.ItemCallback correctly identifies items to avoid incorrect UI updates.
8. Adding Item Decorations
Item decorations provide visual enhancements to RecyclerView items, such as dividers, backgrounds, or spacing. They are drawn on top of the item views.
| Property | Description | Usage |
|---|---|---|
| RecyclerView.ItemDecoration | Base class for item decorations | Extend to customize appearance |
| getItemOffsets() | Defines space around item | Modify layout margins |
| onDraw() | Draws decoration on top of items | Custom drawing logic |
| onDrawOver() | Draws decoration behind items | Overlay effects |
Example: Divider Decoration
class DividerDecoration : RecyclerView.ItemDecoration() {
private val divider = ContextCompat.getDrawable(context, R.drawable.divider)
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val left = parent.paddingLeft.toFloat()
val right = (parent.width - parent.paddingRight).toFloat()
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!!
divider.setBounds(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())
divider.draw(c)
}
}
}
Note: Use
onDrawOver() to draw decorations behind items, and onDraw() for those in front.9. Implementing Swipe to Dismiss
| Feature | Description | Implementation |
|---|---|---|
| Swipe Gesture | Allows users to swipe items left or right to delete or archive them. | Uses ItemTouchHelper with SimpleCallback. |
| Swipe Direction | Supports left-to-right (e.g., delete) and right-to-left (e.g., archive). | Configured via Swiped flags in onMove. |
Example: Swipe to Dismiss Implementation
class SwipeToDeleteCallback(
private val adapter: MyAdapter
) : 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
if (direction == ItemTouchHelper.LEFT) {
adapter.removeItem(position)
}
}
}
Note: Ensure
ItemTouchHelper is attached to the RecyclerView in your activity or fragment.
Setup Steps
- Create a
SwipeToDeleteCallbackclass extendingItemTouchHelper.SimpleCallback. - Override
onSwipedto handle item removal. - Attach the callback to the
RecyclerViewusingItemTouchHelper.
10. Using Multiple View Types
| View Type | Description | Usage |
|---|---|---|
| VIEW_TYPE_HEADER | Represents a header item in the list | Used for displaying section headers or titles |
| VIEW_TYPE_ITEM | Represents a regular list item | Used for displaying content items in the list |
| VIEW_TYPE_FOOTER | Represents a footer item in the list | Used for displaying footers or summary information |
Example: Defining View Types
class MyAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
const val VIEW_TYPE_HEADER = 0
const val VIEW_TYPE_ITEM = 1
const val VIEW_TYPE_FOOTER = 2
}
override fun getItemViewType(position: Int): Int {
return when (position) {
0 -> VIEW_TYPE_HEADER
items.size + 1 -> VIEW_TYPE_FOOTER
else -> VIEW_TYPE_ITEM
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
VIEW_TYPE_HEADER -> HeaderViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.header_item, parent, false)
)
VIEW_TYPE_ITEM -> ItemViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.list_item, parent, false)
)
VIEW_TYPE_FOOTER -> FooterViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.footer_item, parent, false)
)
else -> throw IllegalArgumentException("Unknown view type: $viewType")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is HeaderViewHolder -> holder.bind(headerData)
is ItemViewHolder -> holder.bind(items[position - 1])
is FooterViewHolder -> holder.bind(footerData)
}
}
override fun getItemCount(): Int = items.size + 2
}
Example: ViewHolder Implementations
class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val titleText = itemView.findViewById<TextView>(R.id.title_text)
fun bind(data: HeaderData) {
titleText.text = data.title
}
}
class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val itemText = itemView.findViewById<TextView>(R.id.item_text)
private val itemImage = itemView.findViewById<ImageView>(R.id.item_image)
fun bind(data: ItemData) {
itemText.text = data.title
itemImage.setImageResource(data.imageResId)
}
}
class FooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val footerText = itemView.findViewById<TextView>(R.id.footer_text)
fun bind(data: FooterData) {
footerText.text = data.message
}
}
Note: When using multiple view types, ensure that the
getItemViewType method returns consistent view type values for each position to avoid incorrect view recycling.
11. Implementing Pagination
Example: Basic Pagination Setup
class PaginationAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val items = mutableListOf<String>()
fun addItems(newItems: List<String>) {
items.addAll(newItems)
notifyDataSetChanged()
}
fun loadMore() {
// Simulate loading more items
val newItems = (1..10).map { "Item ${items.size + it}" }
addItems(newItems)
}
}
| Property | Description | Usage |
|---|---|---|
| RecyclerView | UI component for displaying large datasets efficiently | Used to display paginated content in a scrollable list |
| Adapter | Connects data to RecyclerView | Handles item creation and binding for each page |
| LoadMore | Trigger to fetch next page of data | Implement in scroll listener or button click |
Workflow: Implementing Pagination
- Add scroll listener to RecyclerView
- Detect when user reaches end of list
- Fetch next page of data from API
- Update adapter with new items
- Notify RecyclerView of data changes
Note: Use
RecyclerView.Adapter.notifyItemRangeInserted() for better performance when adding items to the end of the list.
Comparison: Pagination Approaches
- Scroll-based: Load more when user scrolls to bottom
- Button-based: User taps "Load More" button to fetch next page
- Infinite: Automatically load pages as needed without user action
12. Handling Drag and Drop
| Feature | Description | Notes |
|---|---|---|
| ItemTouchHelper | Class for handling drag and drop gestures | Part of AndroidX library |
| OnItemTouchListener | Interface to detect touch events on RecyclerView items | Used for custom drag handling |
Example: Basic Drag and Drop Setup
class MainActivity : AppCompatActivity() {
private lateinit var recyclerView: RecyclerView
private lateinit var adapter: MyAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView = findViewById(R.id.recyclerView)
adapter = MyAdapter(dataList)
val itemTouchHelper = ItemTouchHelper(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
adapter.moveItem(fromPosition, toPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
})
itemTouchHelper.attachToRecyclerView(recyclerView)
recyclerView.adapter = adapter
}
}
Note: The
SimpleCallback constructor takes two parameters: dragFlags and swipeFlags. Use ItemTouchHelper.UP or ItemTouchHelper.DOWN for vertical drag.
Example: Custom Drag Handle
class MyAdapter(private val dataList: MutableList<String>) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView: TextView = itemView.findViewById(R.id.textView)
val dragHandle: ImageView = itemView.findViewById(R.id.dragHandle)
init {
dragHandle.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
val itemTouchHelper = ItemTouchHelper(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
dataList.add(toPosition, dataList.removeAt(fromPosition))
notifyItemMoved(fromPosition, toPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
})
itemTouchHelper.startDrag(this@ViewHolder)
}
false
}
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.textView.text = dataList[position]
}
override fun getItemCount(): Int = dataList.size
}