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

在RecyclerView中嵌套RecyclerView是否为最佳实践?(含父子列表场景)

最优方案分析:父子展开折叠列表实现

原嵌套RecyclerView方案的问题

你计划的嵌套RecyclerView方案确实能实现需求,但存在几个明显不足:

  • 布局层级冗余:嵌套RecyclerView会增加View树层级,带来额外的绘制、测量性能开销。
  • ViewHolder复用隐患:若每次父Adapter的onBindViewHolder都初始化子RecyclerView的LayoutManager和Adapter,会造成不必要的对象重复创建;且如果没做好状态缓存,父ViewHolder复用时容易丢失子项的展开状态。
  • 状态维护复杂:需要单独维护每个父项的展开状态,还要确保子RecyclerView的状态在复用后能正确恢复。

更优方案:单一RecyclerView + 多类型Item

这是Android实现展开折叠父子列表的最佳实践,通过将父项和子项定义为RecyclerView的不同类型Item,避免嵌套,简化逻辑同时提升性能。

具体实现步骤

  1. 定义统一的列表Item类型
    用密封类封装父项和子项,同时携带展开状态、父索引等关键信息:
sealed class ListItem {
    // 父项:携带原始Parent数据、展开状态、自身索引
    data class ParentItem(val parent: Parent, val isExpanded: Boolean, val parentIndex: Int) : ListItem()
    // 子项:携带原始Child数据、所属父项的索引
    data class ChildItem(val child: Child, val parentIndex: Int) : ListItem()
}
  1. 构建动态数据源
    维护一个展开状态列表,根据状态生成最终展示的列表:
// 全局维护展开状态
private val expandStates = mutableListOf<Boolean>()

fun buildDisplayList(parentList: List<Parent>): List<ListItem> {
    val displayList = mutableListOf<ListItem>()
    parentList.forEachIndexed { parentIndex, parent ->
        // 添加父项
        displayList.add(ListItem.ParentItem(parent, expandStates[parentIndex], parentIndex))
        // 若展开则添加所有子项
        if (expandStates[parentIndex]) {
            parent.childs.forEach { child ->
                displayList.add(ListItem.ChildItem(child, parentIndex))
            }
        }
    }
    return displayList
}
  1. 实现多类型Adapter
    重写getItemViewType区分父子项类型,创建对应ViewHolder并绑定逻辑:
class ExpandableListAdapter(
    private val parentList: List<Parent>,
    private val onChildClick: (parentIndex: Int, childIndex: Int) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    companion object {
        private const val VIEW_TYPE_PARENT = 0
        private const val VIEW_TYPE_CHILD = 1
    }

    private var displayList: List<ListItem> = emptyList()

    init {
        expandStates.addAll(List(parentList.size) { false })
        displayList = buildDisplayList(parentList)
    }

    override fun getItemViewType(position: Int): Int {
        return when (displayList[position]) {
            is ListItem.ParentItem -> VIEW_TYPE_PARENT
            is ListItem.ChildItem -> VIEW_TYPE_CHILD
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            VIEW_TYPE_PARENT -> {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.item_parent, parent, false)
                ParentViewHolder(view)
            }
            else -> {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.item_child, parent, false)
                ChildViewHolder(view)
            }
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (val item = displayList[position]) {
            is ListItem.ParentItem -> {
                (holder as ParentViewHolder).bind(item) {
                    // 切换展开状态
                    expandStates[item.parentIndex] = !expandStates[item.parentIndex]
                    val newDisplayList = buildDisplayList(parentList)
                    val startPos = position + 1
                    val childCount = item.parent.childs.size
                    // 局部刷新,避免全量更新
                    if (expandStates[item.parentIndex]) {
                        displayList = newDisplayList
                        notifyItemRangeInserted(startPos, childCount)
                    } else {
                        displayList = newDisplayList
                        notifyItemRangeRemoved(startPos, childCount)
                    }
                }
            }
            is ListItem.ChildItem -> {
                (holder as ChildViewHolder).bind(item) {
                    // 计算子项索引并回调
                    val childIndex = parentList[item.parentIndex].childs.indexOf(item.child)
                    onChildClick(item.parentIndex, childIndex)
                }
            }
        }
    }

    override fun getItemCount(): Int = displayList.size

    // 父项ViewHolder:绑定布局、处理点击切换展开状态
    inner class ParentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bind(item: ListItem.ParentItem, onToggle: () -> Unit) {
            itemView.tv_title.text = item.parent.title
            // 设置展开/折叠图标等UI
            itemView.setOnClickListener { onToggle() }
        }
    }

    // 子项ViewHolder:绑定布局、处理点击回调
    inner class ChildViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bind(item: ListItem.ChildItem, onClick: () -> Unit) {
            itemView.tv_title.text = item.child.title
            itemView.setOnClickListener { onClick() }
        }
    }
}
  1. 状态与回调逻辑
  • 展开/折叠状态通过expandStates统一维护,切换时仅局部刷新列表,性能更优。
  • 子项点击时,通过ChildItem携带的parentIndex结合原始父项数据,可直接获取子项索引并回调。

两种方案的适用场景

  • 嵌套RecyclerView:仅适合父子项业务逻辑完全独立、子项需要复杂滚动交互(如横向滚动)的场景;若只是垂直固定高度的子项,该方案性价比极低。
  • 单一RecyclerView多类型:几乎适用于所有展开折叠父子列表场景,布局层级更浅、性能更优,状态维护更简单,是Android官方推荐的实现方式。

若你坚持使用嵌套RecyclerView,可做以下优化:

  • 在父ViewHolder中缓存子Adapter和LayoutManager,避免每次onBindViewHolder重复创建。
  • 用单独列表保存父项展开状态,onBindViewHolder时根据状态设置子RecyclerView可见性。
  • 给子RecyclerView设置childRecyclerView.setNestedScrollingEnabled(false),避免嵌套滚动冲突。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 15:50:27