嵌套RecyclerView添加父项后ItemDecoration边距异常问题
我用DelegateAdapters在父RecyclerView中展示不同分区,初始添加两个带标题和横向嵌套列表的分区(AuthorTracksDelegateItem和NewReleasesDelegateItem),同时通过观察LiveData,在有新曲目时添加第三个分区(PlayerDelegateItem),代码如下:
fun setAdapter(recyclerView: RecyclerView){ //Add sections val testData = listOf( AuthorTracksDelegateItem(title = "Beatles", tracks = homeViewModel.getTracks("Beatles")), NewReleasesDelegateItem(title = "New releases", albums = homeViewModel.getNewReleases()), ) compositeAdapter.setItems(testData) //Observe and add new section homeViewModel.getTracks("Imagine dragons").observe(viewLifecycleOwner) { tracks -> if(tracks.isNotEmpty()){ compositeAdapter.addToEnd(PlayerDelegateItem(title = "Last play", content = tracks.first())) } } //other recyclerView.layoutManager = LinearLayoutManager(context) recyclerView.adapter = compositeAdapter val padding = resources.getDimensionPixelSize(R.dimen.section_margin) recyclerView.addItemDecoration(MarginItemDecoration(padding, padding*10, padding)) }
随后在ViewHolder中创建嵌套横向列表,代码如下:
inner class NewReleasesViewHolder(itemView: View): DelegateViewHolder(itemView) { private val title = itemView.findViewById<TextView>(R.id.section_title) private val recyclerView = itemView.findViewById<RecyclerView>(R.id.rv_horizontal_tracks) private val adapter = NewReleasesAdapter() override fun bind(item: IDelegateAdapterItem) { title.text = item.id() as String (item.content() as LiveData<List<Album>>).observeForever { albums -> adapter.setItems(albums) } setupRecycleView() } private fun setupRecycleView(){ val layout = LinearLayoutManager(itemView.context) layout.orientation = LinearLayoutManager.HORIZONTAL recyclerView.adapter = adapter recyclerView.layoutManager = layout recyclerView.addItemDecoration( MarginItemDecoration( startSpace = itemView.resources.getDimensionPixelSize(R.dimen.content_horizontal_margin), endSpace = itemView.resources.getDimensionPixelSize(R.dimen.content_horizontal_margin), betweenSpace = itemView.resources.getDimensionPixelSize(R.dimen.items_margin), isHorizontal = true ) ) } } override fun createViewHolder(binding: View): RecyclerView.ViewHolder { return NewReleasesViewHolder(binding) } override fun getLayoutId(): Int { return R.layout.section_nested_list } inner class TrackViewHolder(itemView: View): DelegateViewHolder(itemView){ val title = itemView.findViewById<TextView>(R.id.section_title) val recyclerView = itemView.findViewById<RecyclerView>(R.id.rv_horizontal_tracks) val adapter = LargeTracksAdapter() override fun bind(item: IDelegateAdapterItem) { title.text = item.id() as String (item.content() as LiveData<List<Track>>).observeForever { tracks -> adapter.setItems(tracks) } setupRecycleView() } fun setupRecycleView(){ val layout = LinearLayoutManager(itemView.context) layout.orientation = LinearLayoutManager.HORIZONTAL recyclerView.layoutManager = layout recyclerView.adapter = adapter recyclerView.addItemDecoration(MarginItemDecoration( startSpace = itemView.resources.getDimensionPixelSize(R.dimen.content_horizontal_margin), endSpace = itemView.resources.getDimensionPixelSize(R.dimen.content_horizontal_margin), betweenSpace = itemView.resources.getDimensionPixelSize(R.dimen.items_margin), isHorizontal = true )) recyclerView.invalidateItemDecorations() } }
我用MarginItemDecoration设置元素间边距,代码如下:
class MarginItemDecoration( private val startSpace: Int = 0, private val endSpace: Int = 0, private var betweenSpace: Int = 0, private val isHorizontal: Boolean = false): RecyclerView.ItemDecoration() { init { betweenSpace /= 2 } override fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State ) { val size = parent.adapter?.itemCount ?: 0 val itemPosition = parent.getChildAdapterPosition(view) if (isHorizontal) { outRect.left = getStartSpace(itemPosition) outRect.right = getEndSpace(itemPosition, size) } else { outRect.top = getStartSpace(itemPosition) outRect.bottom = getEndSpace(itemPosition, size) } } private fun getStartSpace( itemPosition: Int ): Int = when (itemPosition) { 0 -> startSpace else -> betweenSpace } private fun getEndSpace( itemPosition: Int, size: Int, ): Int = when (itemPosition) { size-1 -> endSpace else -> betweenSpace } }
问题现象
尽管使用dimensions.xml中的固定值,部分分区的嵌套列表边距却翻倍。添加多个分区后出现更复杂的异常:
//Add sections val testData = listOf( AuthorTracksDelegateItem(title = "Beatles", tracks = homeViewModel.getTracks("Beatles")), NewReleasesDelegateItem(title = "New releases", albums = homeViewModel.getNewReleases()), AuthorTracksDelegateItem(title = "Linkin Park", tracks = homeViewModel.getTracks("Linkin Park")), AuthorTracksDelegateItem(title = "21 pilots", tracks = homeViewModel.getTracks("21 pilots")), )
实际表现:
- 第一个分区(Beatles)显示正常
- 第二个分区(New releases)嵌套列表边距翻倍
- 第三个分区(Linkin Park)嵌套列表边距正常,但自身底部边距异常增大
- 第四个分区(21 pilots)嵌套列表边距再次异常
已尝试方案
发现父RecyclerView添加新分区后边距异常,尝试延迟添加、调用invalidateItemDecorations()、更换布局和ID均无效。
问题原因与解决方法
核心问题:重复添加ItemDecoration
你的setupRecycleView()方法在每次bind()时都会被调用,而每次调用都会给嵌套RecyclerView添加一个新的MarginItemDecoration。RecyclerView的ItemDecoration是累加的,多次添加就会导致边距翻倍、异常增大的问题。另外,observeForever()未解绑,会触发重复绑定,加剧Decoration重复添加的问题。
具体修复步骤:
- 避免重复添加ItemDecoration
把MarginItemDecoration的初始化移到ViewHolder的构造方法中,只添加一次:
inner class NewReleasesViewHolder(itemView: View): DelegateViewHolder(itemView) { private val title = itemView.findViewById<TextView>(R.id.section_title) private val recyclerView = itemView.findViewById<RecyclerView>(R.id.rv_horizontal_tracks) private val adapter = NewReleasesAdapter() init { // 初始化时只添加一次Decoration setupRecycleView() } override fun bind(item: IDelegateAdapterItem) { title.text = item.id() as String val liveData = item.content() as LiveData<List<Album>> liveData.observeForever { albums -> adapter.setItems(albums) } // 移除bind中的setupRecycleView调用 } private fun setupRecycleView(){ val layout = LinearLayoutManager(itemView.context) layout.orientation = LinearLayoutManager.HORIZONTAL recyclerView.adapter = adapter recyclerView.layoutManager = layout recyclerView.addItemDecoration( MarginItemDecoration( startSpace = itemView.resources.getDimensionPixelSize(R.dimen.content_horizontal_margin), endSpace = itemView.resources.getDimensionPixelSize(R.dimen.content_horizontal_margin), betweenSpace = itemView.resources.getDimensionPixelSize(R.dimen.items_margin), isHorizontal = true ) ) } }
TrackViewHolder做同样修改,把setupRecycleView()移到构造方法的init块中,不在bind()里重复调用。
- 正确处理LiveData的观察
使用observe()而非observeForever(),并在ViewHolder销毁时移除观察者,避免内存泄漏和重复回调:
// 在ViewHolder中添加观察者引用 private var albumObserver: Observer<List<Album>>? = null override fun bind(item: IDelegateAdapterItem) { title.text = item.id() as String val liveData = item.content() as LiveData<List<Album>> // 先移除之前的观察者 albumObserver?.let { liveData.removeObserver(it) } // 创建新的观察者 albumObserver = Observer { albums -> adapter.setItems(albums) } liveData.observe(viewLifecycleOwner, albumObserver!!) } // 在ViewHolder的onViewDetachedFromWindow中移除观察者 override fun onViewDetachedFromWindow() { super.onViewDetachedFromWindow() val liveData = item.content() as LiveData<List<Album>> albumObserver?.let { liveData.removeObserver(it) } }
若DelegateViewHolder没有onViewDetachedFromWindow方法,需重载该方法,或在合适的生命周期节点解绑观察者。
- 调整父RecyclerView的Decoration参数
父RecyclerView的MarginItemDecoration设置了endSpace = padding*10,过大的值可能导致分区底部边距异常,建议统一使用padding作为endSpace:
recyclerView.addItemDecoration(MarginItemDecoration(padding, padding, padding))
验证修复
修改后重新运行,检查各分区边距是否恢复正常,同时确认内存泄漏问题是否解决。
内容的提问来源于stack exchange,提问作者Ikrom

