You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

RecyclerView动态头部实现问题:API数据按月份分组展示

Hey there! Let's break down how to implement this grouped RecyclerView with month headers, even when dealing with dynamic API data that has variable item counts. Here's a practical, step-by-step solution:

1. First: Preprocess Your API Data

The first thing you need to do is transform your raw API response into a structured list that mixes month headers and their corresponding content items.

First, define a sealed class to distinguish between header and content items (this makes handling view types easier later):

sealed class ListItem {
    data class MonthHeader(val monthName: String) : ListItem()
    data class ContentItem(val data: YourApiModel) : ListItem()
}

Next, write a function to group your API data by month and build the mixed list:

fun processApiData(apiData: List<YourApiModel>): List<ListItem> {
    // Group raw data by month (assume YourApiModel has a 'month' field like "MAR")
    val groupedByMonth = apiData.groupBy { it.month }
    
    val processedList = mutableListOf<ListItem>()
    
    // Optional but critical: Sort months chronologically (not random API order)
    val monthOrderMap = mapOf(
        "JAN" to 1, "FEB" to 2, "MAR" to 3, "APR" to 4,
        "MAY" to 5, "JUN" to 6, "JUL" to 7, "AUG" to 8,
        "SEP" to 9, "OCT" to 10, "NOV" to 11, "DEC" to 12
    )
    val sortedGroups = groupedByMonth.entries.sortedBy { monthOrderMap[it.key] }
    
    // Populate the list with headers followed by their content
    sortedGroups.forEach { (month, items) ->
        processedList.add(ListItem.MonthHeader(month))
        items.forEach { processedList.add(ListItem.ContentItem(it)) }
    }
    
    return processedList
}
2. Build a Multi-ViewType RecyclerView Adapter

Since you have two distinct item types (headers and content), your adapter needs to handle both. Here's a clean implementation:

class GroupedMonthAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    private val items = mutableListOf<ListItem>()
    
    companion object {
        private const val VIEW_TYPE_HEADER = 0
        private const val VIEW_TYPE_CONTENT = 1
    }

    fun updateData(newItems: List<ListItem>) {
        val diffCallback = ListItemDiffCallback(items, newItems)
        val diffResult = DiffUtil.calculateDiff(diffCallback)
        
        items.clear()
        items.addAll(newItems)
        diffResult.dispatchUpdatesTo(this)
    }

    override fun getItemViewType(position: Int): Int {
        return when (items[position]) {
            is ListItem.MonthHeader -> VIEW_TYPE_HEADER
            is ListItem.ContentItem -> VIEW_TYPE_CONTENT
        }
    }

    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.item_month_header, parent, false)
                HeaderViewHolder(view)
            }
            else -> {
                val view = inflater.inflate(R.layout.item_content, parent, false)
                ContentViewHolder(view)
            }
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (val item = items[position]) {
            is ListItem.MonthHeader -> (holder as HeaderViewHolder).bind(item)
            is ListItem.ContentItem -> (holder as ContentViewHolder).bind(item.data)
        }
    }

    override fun getItemCount() = items.size

    // ViewHolder classes
    class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bind(header: ListItem.MonthHeader) {
            itemView.tv_month_header.text = header.monthName
            // Add styling here (bold text, background color, padding, etc.)
        }
    }

    class ContentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bind(data: YourApiModel) {
            // Bind your content data to views
            itemView.tv_title.text = data.title
            itemView.tv_details.text = data.details
        }
    }
}
3. Optimize Updates with DiffUtil

To avoid janky full-list refreshes when your API data updates, use DiffUtil to only update changed items. Add this callback class:

class ListItemDiffCallback(
    private val oldList: List<ListItem>,
    private val newList: List<ListItem>
) : DiffUtil.Callback() {
    override fun getOldListSize() = oldList.size
    override fun getNewListSize() = newList.size

    override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
        val oldItem = oldList[oldPos]
        val newItem = newList[newPos]
        return when {
            oldItem is ListItem.MonthHeader && newItem is ListItem.MonthHeader -> 
                oldItem.monthName == newItem.monthName
            oldItem is ListItem.ContentItem && newItem is ListItem.ContentItem -> 
                oldItem.data.id == newItem.data.id // Use your model's unique ID
            else -> false
        }
    }

    override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
        return oldList[oldPos] == newList[newPos]
        // Ensure your data classes (ListItem and YourApiModel) are 'data classes' so equals() works
    }
}
4. Bonus: Add Sticky Headers (If Needed)

If you want headers to stay fixed at the top while scrolling, you can implement a RecyclerView.ItemDecoration to draw and position the sticky header. Here's a simplified snippet to get you started:

class StickyHeaderDecoration(private val adapter: GroupedMonthAdapter) : RecyclerView.ItemDecoration() {
    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(c, parent, state)
        val topChild = parent.getChildAt(0) ?: return
        val topChildPos = parent.getChildAdapterPosition(topChild)
        if (topChildPos == RecyclerView.NO_POSITION) return

        val currentHeader = getHeaderView(topChildPos, parent)
        val contactPoint = currentHeader.bottom
        val childInContact = getChildInContact(parent, contactPoint) ?: return

        if (isHeader(parent.getChildAdapterPosition(childInContact))) {
            moveHeader(c, currentHeader, childInContact)
            return
        }

        drawHeader(c, currentHeader)
    }

    // Helper methods to fetch header views, check if an item is a header, etc.
    private fun getHeaderView(position: Int, parent: RecyclerView): View {
        val headerItem = adapter.items[position] as ListItem.MonthHeader
        val holder = adapter.onCreateViewHolder(parent, GroupedMonthAdapter.VIEW_TYPE_HEADER) as GroupedMonthAdapter.HeaderViewHolder
        holder.bind(headerItem)
        holder.itemView.measure(
            View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY),
            View.MeasureSpec.makeMeasureSpec(View.LayoutParams.WRAP_CONTENT, View.MeasureSpec.UNSPECIFIED)
        )
        holder.itemView.layout(0, 0, holder.itemView.measuredWidth, holder.itemView.measuredHeight)
        return holder.itemView
    }

    private fun isHeader(position: Int): Boolean {
        return adapter.items[position] is ListItem.MonthHeader
    }

    private fun getChildInContact(parent: RecyclerView, contactY: Int): View? {
        for (i in 0 until parent.childCount) {
            val child = parent.getChildAt(i)
            if (child.bottom > contactY && child.top <= contactY) {
                return child
            }
        }
        return null
    }

    private fun moveHeader(c: Canvas, header: View, child: View) {
        c.save()
        c.translate(0f, (child.top - header.height).toFloat())
        header.draw(c)
        c.restore()
    }

    private fun drawHeader(c: Canvas, header: View) {
        c.save()
        c.translate(0f, 0f)
        header.draw(c)
        c.restore()
    }
}

Then attach it to your RecyclerView:

recyclerView.addItemDecoration(StickyHeaderDecoration(adapter))

That's the full workflow! The core idea is transforming your ungrouped API data into a list that alternates headers and content, then letting the adapter handle the different view types. This setup works seamlessly no matter how many items your API returns.

内容的提问来源于stack exchange,提问作者Maulik Dodia

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 06:39:12