RecyclerView Kotlin: How to Add Header and Footer in XML Development

In Android development with Kotlin and XML, the RecyclerView is a fundamental UI component for displaying dynamic lists of data. Enhancing the RecyclerView with headers and footers can greatly improve user experience and provide additional context. This comprehensive guide explores various methods to add headers and footers to your RecyclerView, along with code examples.

Why Add Headers and Footers to RecyclerView?

  • Improved User Experience: Headers provide context or introductory information at the top, while footers offer concluding remarks, calls to action, or pagination controls at the bottom.
  • Enhanced Content Presentation: They allow for better organization and navigation through long lists of data.
  • Dynamic Updates: Headers and footers can display summary data or dynamically update based on list content.

Method 1: Using Multiple View Types

One common approach is to use different view types within the same RecyclerView.Adapter to represent header, item, and footer views. This method offers great flexibility and is well-suited for complex scenarios.

Step 1: Define View Types

Create integer constants for each view type you want to display:

private const val VIEW_TYPE_HEADER = 0
private const val VIEW_TYPE_ITEM = 1
private const val VIEW_TYPE_FOOTER = 2

Step 2: Modify the Adapter

Update your RecyclerView.Adapter to handle different view types. Start by checking if the adapter contains the data for the header and footer

class MyAdapter(private val dataSet: MutableList<String>, private val hasHeader: Boolean, private val hasFooter: Boolean) :
    RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    override fun getItemViewType(position: Int): Int {
        return when (position) {
            0 -> if (hasHeader) VIEW_TYPE_HEADER else VIEW_TYPE_ITEM
            itemCount - 1 -> if (hasFooter) VIEW_TYPE_FOOTER else VIEW_TYPE_ITEM
            else -> VIEW_TYPE_ITEM
        }
    }

Next, define the ViewHolder class. It holds reference for each view and determine which layout is used for view type


    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        return when (viewType) {
            VIEW_TYPE_HEADER -> {
                val view = inflater.inflate(R.layout.header_layout, parent, false)
                HeaderViewHolder(view)
            }
            VIEW_TYPE_FOOTER -> {
                val view = inflater.inflate(R.layout.footer_layout, parent, false)
                FooterViewHolder(view)
            }
            else -> {
                val view = inflater.inflate(R.layout.item_layout, parent, false)
                ItemViewHolder(view)
            }
        }
    }

Third, Bind the each view using below coding and remove the item from original dataset accordingly. In onBindViewHolder method, implement the logic to bind data to the header, item, and footer views based on their respective view types


    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is HeaderViewHolder -> {
                // Bind header data
                holder.headerTitle.text = "This is a Header"
            }
            is FooterViewHolder -> {
                // Bind footer data
                holder.footerTitle.text = "This is a Footer"
            }
            is ItemViewHolder -> {
                // Adjust position to account for the header
                val item = dataSet[if (hasHeader) position - 1 else position]
                holder.itemTitle.text = item
            }
        }
    }

Next, Modify the item count and set header , item , footer respectively.


    override fun getItemCount(): Int {
        var itemCount = dataSet.size
        if (hasHeader) itemCount++
        if (hasFooter) itemCount++
        return itemCount
    }

Define three ViewHolder which extends RecyclerView respectively:

 class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val headerTitle: TextView = itemView.findViewById(R.id.headerTitle)
    }

    class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val itemTitle: TextView = itemView.findViewById(R.id.itemTitle)
    }

    class FooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val footerTitle: TextView = itemView.findViewById(R.id.footerTitle)
    }
}

Step 3: Create Layout Files

Define the XML layout files for the header, item, and footer:

  • header_layout.xml
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:id="@+id/headerTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Header"
        android:textSize="20sp"
        android:gravity="center"/>

</LinearLayout>
  • item_layout.xml
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:id="@+id/itemTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Item"
        android:textSize="16sp"
        android:gravity="center"/>

</LinearLayout>
  • footer_layout.xml
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:id="@+id/footerTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Footer"
        android:textSize="20sp"
        android:gravity="center"/>

</LinearLayout>

Method 2: Using ConcatAdapter

ConcatAdapter simplifies combining multiple adapters into a single one. It is beneficial when you have distinct data sources for headers, items, and footers.

Step 1: Add Dependency

Make sure to include ConcatAdapter dependency in your build.gradle:

dependencies {
    implementation "androidx.recyclerview:recyclerview:1.3.2"
    implementation "androidx.recyclerview:recyclerview-selection:1.1.0"
}

Step 2: Create Individual Adapters

Create separate adapters for the header, main content, and footer.

  • Header Adapter:
class HeaderAdapter(private val headerText: String) :
    RecyclerView.Adapter<HeaderAdapter.HeaderViewHolder>() {

    class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val textView: TextView = itemView.findViewById(R.id.headerTextView)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.header_layout, parent, false)
        return HeaderViewHolder(view)
    }

    override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) {
        holder.textView.text = headerText
    }

    override fun getItemCount(): Int = 1
}
  • Item Adapter:
class ItemAdapter(private val items: List<String>) :
    RecyclerView.Adapter<ItemAdapter.ItemViewHolder>() {

    class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val textView: TextView = itemView.findViewById(R.id.itemTextView)
    }

    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.textView.text = items[position]
    }

    override fun getItemCount(): Int = items.size
}
  • Footer Adapter:
class FooterAdapter(private val footerText: String) :
    RecyclerView.Adapter<FooterAdapter.FooterViewHolder>() {

    class FooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val textView: TextView = itemView.findViewById(R.id.footerTextView)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FooterViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.footer_layout, parent, false)
        return FooterViewHolder(view)
    }

    override fun onBindViewHolder(holder: FooterViewHolder, position: Int) {
        holder.textView.text = footerText
    }

    override fun getItemCount(): Int = 1
}

Step 3: Combine Adapters Using ConcatAdapter

Create a ConcatAdapter and add the individual adapters in the desired order.

val headerAdapter = HeaderAdapter("This is a Header")
val itemAdapter = ItemAdapter(listOf("Item 1", "Item 2", "Item 3"))
val footerAdapter = FooterAdapter("This is a Footer")

val concatAdapter = ConcatAdapter(headerAdapter, itemAdapter, footerAdapter)

recyclerView.adapter = concatAdapter

Method 3: Using ListAdapter with Data Classes

When employing ListAdapter along with data classes, integrate the header and footer by treating them as list items and differentiate through data class properties.

Step 1: Create Sealed Class and Data Classes

Create a sealed class and data classes to represent different item types:

sealed class RecyclerViewItem {
    data class Header(val text: String) : RecyclerViewItem()
    data class Item(val text: String) : RecyclerViewItem()
    data class Footer(val text: String) : RecyclerViewItem()
}

Step 2: Modify the Adapter

Adjust your ListAdapter to work with the sealed class. Make the dataset with RecylerView Item

class MyListAdapter :
    ListAdapter<RecyclerViewItem, RecyclerView.ViewHolder>(RecyclerViewItemDiffCallback()) {

    override fun getItemViewType(position: Int): Int {
        return when (getItem(position)) {
            is RecyclerViewItem.Header -> VIEW_TYPE_HEADER
            is RecyclerViewItem.Item -> VIEW_TYPE_ITEM
            is RecyclerViewItem.Footer -> VIEW_TYPE_FOOTER
        }
    }

Create the adapter based on getItemViewType parameter from the override method.

 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        return when (viewType) {
            VIEW_TYPE_HEADER -> {
                val view = inflater.inflate(R.layout.header_layout, parent, false)
                HeaderViewHolder(view)
            }
            VIEW_TYPE_FOOTER -> {
                val view = inflater.inflate(R.layout.footer_layout, parent, false)
                FooterViewHolder(view)
            }
            else -> {
                val view = inflater.inflate(R.layout.item_layout, parent, false)
                ItemViewHolder(view)
            }
        }
    }

Define how each view item bind on the layout in method onBindViewHolder. Header and footer is on top and the button respectively. Remove the position from dataset since list always at position 0.


    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is HeaderViewHolder -> {
                val header = getItem(position) as RecyclerViewItem.Header
                holder.headerTitle.text = header.text
            }
            is ItemViewHolder -> {
                val item = getItem(position) as RecyclerViewItem.Item
                holder.itemTitle.text = item.text
            }
            is FooterViewHolder -> {
                val footer = getItem(position) as RecyclerViewItem.Footer
                holder.footerTitle.text = footer.text
            }
        }
    }

Implement ViewHolder Class same way from Method 1. Define three ViewHolder which extends RecyclerView respectively:

  class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val headerTitle: TextView = itemView.findViewById(R.id.headerTitle)
    }

    class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val itemTitle: TextView = itemView.findViewById(R.id.itemTitle)
    }

    class FooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val footerTitle: TextView = itemView.findViewById(R.id.footerTitle)
    }

To calculate what item needs to re-render if item is same as each other .Using call back to differentiate items that were used.

class RecyclerViewItemDiffCallback : DiffUtil.ItemCallback<RecyclerViewItem>() {
    override fun areItemsTheSame(oldItem: RecyclerViewItem, newItem: RecyclerViewItem): Boolean {
        return when {
            oldItem is RecyclerViewItem.Header && newItem is RecyclerViewItem.Header ->
                oldItem.text == newItem.text
            oldItem is RecyclerViewItem.Item && newItem is RecyclerViewItem.Item ->
                oldItem.text == newItem.text
            oldItem is RecyclerViewItem.Footer && newItem is RecyclerViewItem.Footer ->
                oldItem.text == newItem.text
            else -> false
        }
    }

    override fun areContentsTheSame(oldItem: RecyclerViewItem, newItem: RecyclerViewItem): Boolean {
        return oldItem == newItem
    }
}

Setup the data into recylerview when it initialized, Create MutableList based on structure that defines at above to add list to your own


        val items = mutableListOf(
            RecyclerViewItem.Header("Header"),
            RecyclerViewItem.Item("Item 1"),
            RecyclerViewItem.Item("Item 2"),
            RecyclerViewItem.Footer("Footer")
        )

        val adapter = MyListAdapter()
        recyclerView.adapter = adapter
        adapter.submitList(items)

Conclusion

Adding headers and footers to a RecyclerView significantly enhances its functionality and user interface. Whether you use multiple view types, ConcatAdapter, or ListAdapter with data classes, each method provides flexibility and structure to improve content presentation in Android apps with Kotlin and XML. Consider the complexity of your data and UI requirements when choosing the best approach.