Android RecyclerView Header/Footer 추가하는 가장 쉬운 방법 (ViewType 활용)

✨ 개요

Android 앱에서 리스트 상단에 공지, 타이틀, 필터 등을 넣고 싶거나, 하단에 로딩 인디케이터 또는 “더보기” 버튼 등을 붙이고 싶을 때는 RecyclerView에 Header/Footer 추가 기능이 필요합니다.

이번 포스팅에서는 ViewType을 사용해 RecyclerView에 Header/Footer를 추가하는 방법을 소개합니다.


1. ✅ ViewType 구조 이해

RecyclerView는 getItemViewType(position: Int)를 통해 각 아이템에 다른 ViewHolder를 매핑할 수 있는 구조를 제공합니다.
이 구조를 활용하면 쉽게 Header, Footer를 붙일 수 있습니다.


2. ✅ 예제

2.1 layout 코드

<TextView
        android:id="@+id/headerTextView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:gravity="center"
        android:text="📢 헤더: 공지사항 클릭!"
        android:textStyle="bold"
        android:textSize="18sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
<Button
        android:id="@+id/footerButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:text="더보기"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
<TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:textSize="18sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

2.2 Adapter 코드

data class Item(
    val name: String
)

class MyAdapter(
    private val items: List<Item>,
    val itemClick: ((Int, Item) -> Unit),
    val onHeaderClick: (() -> Unit)? = null,
    val onFooterClick: (() -> Unit)? = null,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    companion object {
        private const val VIEW_TYPE_HEADER = 0
        private const val VIEW_TYPE_ITEM = 1
        private const val VIEW_TYPE_FOOTER = 2
    }

    override fun getItemCount(): Int = items.size + 2 // +1 header, +1 footer

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

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val inflater = LayoutInflater.from(parent.context)

        return when (viewType) {
            VIEW_TYPE_HEADER -> {
                val binding =
                    ItemHeaderBinding.inflate(inflater, parent, false)
                HeaderViewHolder(binding)
            }

            VIEW_TYPE_FOOTER -> {
                val binding =
                    ItemFooterBinding.inflate(inflater, parent, false)
                FooterViewHolder(binding)
            }

            else -> {
                val binding =
                    ItemMyBinding.inflate(inflater, parent, false)
                ItemViewHolder(binding)
            }
        }
    }

    inner class HeaderViewHolder(private val binding: ItemHeaderBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind() {
            binding.headerTextView.setOnClickListener {
                onHeaderClick?.invoke()
            }
        }
    }

    inner class FooterViewHolder(private val binding: ItemFooterBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind() {
            binding.footerButton.setOnClickListener {
                onFooterClick?.invoke()
            }
        }
    }

    inner class ItemViewHolder(private val binding: ItemMyBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(item: Item) {
            binding.textView.text = item.name

            binding.root.setOnClickListener {
                itemClick.invoke(adapterPosition-1, item)
            }
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is HeaderViewHolder -> holder.bind()
            is FooterViewHolder -> holder.bind()
            is ItemViewHolder -> {
                val item = items[position - 1] // offset for header
                holder.bind(item)
            }
        }
    }
}

2.3 Adapter 호출 코드

val list = mutableListOf<Item>()
repeat(100) {
    list.add(Item("$it name"))
}

val adapter = MyAdapter(list,
    itemClick = { position, item ->
        Log.e(TAG, "itemClick $position $item")
    } ,
    onHeaderClick = {
        Log.e(TAG, "onHeaderClick")
    },
    onFooterClick = {
        Log.e(TAG, "onFooterClick")
    }
)
binding.recyclerView.adapter = adapter

3. 🔍 정리

항목 설명
ViewType 활용 위치에 따라 다른 레이아웃을 처리 가능
Header/Footer 추가 별도 XML + ViewHolder 구성 필요
데이터 오프셋 처리 실제 데이터는 position-1부터 시작
확장성 광고, 추천 항목, 로딩 인디케이터 등 다양하게 활용 가능

4.🧠 결론



Related Posts