androidkotlinandroid-recyclerviewadapter

ItemDecoration in nested RecycleView changes paddings after adding new item in parent RecycleView


What am I doing

I use DelegateAdapters to display different sections in the parent RecyclerView. In this code I am adding two sections (AuthorTracksDelegateItem and NewReleasesDelegateItem) that have titles and a list of tracks that will then be displayed in nested horizontal lists. Also, I will observe LiveData so that I can add a third section (PlayerDelegateItem) when another track appears and then everything is as usual

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))
}

After that, already in ViewHolders, I also create new nested lists, everything is as usual.

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()
    }
}

In order to add padding between elements I use ItemDecoration

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
}

}

What is the problem?

Although I used the same and fixed values from dimensions.xml, the indentation shows different, for some reason, in the second section it is twice as large screenshot

What have I tried

I noticed that the indents break when a new section is added to the parent RecyclerView, I tried setting the delay to 5 seconds. Everything was fine, but after adding the indents they broke again. I tried adding invalidateItemDecorations() but that didn't help either. Maybe I’m somewhere in the wrong place or didn’t use them correctly, since a new section is added to the parent RecyclerView, and the indents are broken in the nested ones. I tried using different layouts and different IDs, but nothing helped. I also tried to add several sections, just duplicated them, but something completely strange happened

//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")),
    )

I expected that in each subsequent section the space between elements would double, but this is what happened: The section that was added via observe (even if it was zero) was displayed normally, but there were no nested lists there

  1. The first section (Beatles) was also displayed normally
  2. The second section (New releases) was already double indented in the nested list
  3. The third section (Linkin Park) also had normal padding in the nested list, but the section itself had increased padding at the bottom (I did this for the last element so that it would not overlap with the NavBar, but it was not the last element)
  4. The fourth section (21 pilots) is the same class as the previous one, but it again had incorrect indentations in the nested list

I really don’t understand why the hell he behaves like this, why sections 1, 2 and 4, which are of the same class, are displayed completely differently source code: https://github.com/Ikrom27/music-club-classic


Solution

  • Perhaps, when adding a new element, all nested lists are redrawn again and a second itemDecoration is added to them. Therefore, each time you need to either remove all decorations or before adding, check that the list has no other decorations

    if (recyclerView.itemDecorationCount == 0) {
        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
        ))
    }