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.