Thomas Meinhart
Thomas Meinhart

Reputation: 689

RecyclerView doesn't display last item when multiple ViewHolders are used

I'm using androidx.recyclerview.widget.RecyclerView to display a list of items, separated by an other item as a "header" with some aggregated values.

When i put only one item in my list without adding the header, everything is ok and the item is displayed correctly. As soon as i add the header item, only the header is displayed and the one single item isn't shown. When i add two items and the header, the header and one item are displayed. I don't know why the last item of my list is missing altough it exists in the adapters datasource.

My ListAdapter inherits from RecyclerView.Adapter<RecyclerView.ViewHolder> and uses two ViewHolders detected by a viewType property of my list items.

When loading the data, the onBindViewHolder method isn't called for the last item in my list, even tough the item is in the visible section of my screen.

Does anybody has a hint, why this happens?

class ListAdapter(val onClick: (position: Long) -> Unit,
                      val onLongClick: (Long) -> Unit,
                      val onShareClick: (id: Long?) -> Unit) : RecyclerView.Adapter<RecyclerView.ViewHolder>(),
    BindableAdapter<List<ListAdapterItem<*>>> {

    var items: List<ListAdapterItem<*>> = emptyList()

    private var actionMode: ActionMode? = null
    var tracker: SelectionTracker<Long>? = null

    init {
        setHasStableIds(true)
    }

    override fun setData(data: List<ListAdapterItem<*>>) {
        this.items = data // all items are set correctly here!!
        notifyDataSetChanged()
    }

    override fun getItemViewType(position: Int): Int {
        return if (items.isEmpty()) EMPTY else items[position].viewType
    }

    override fun getItemCount(): Int {
        return if (items.isEmpty()) 1 else items.filter { it.viewType == ITEM }.size
    }

    override fun getItemId(position: Int): Long = position.toLong()

    fun getItem(position: Long): ListViewModel.ListItem = item[position.toInt()].value as ListViewModel.ListItem

    fun setActionMode(actionMode: ActionMode?) {
        this.actionMode = actionMode
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            EMPTY -> EmptyViewHolder(parent)
            HEADER -> HistoryGroupHeaderViewHolder(parent)
            else -> HistoryViewHolder(parent)
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (holder is HistoryViewHolder) { 
            val item = items[position].value as ListViewModel.ListItem

            tracker?.let {
                holder.bind(item, it.isSelected(position.toLong()))
            }
            holder.itemView.setOnClickListener {
                onClick(position.toLong())
            }
            holder.itemView.findViewById<AppCompatImageView>(R.id.history_item_share)?.setOnClickListener {
                onShareClick(item.id)
            }
        }
        else if (holder is HistoryGroupHeaderViewHolder) { 
            val header = items[position].value as ListViewModel.ListSectionHeader
            holder.bind(header)
        }
    }

    class HistoryViewHolder(
        private val parent: ViewGroup,
        private val binding: at.app.databinding.ViewHistoryListItemBinding = DataBindingUtil.inflate(
            LayoutInflater.from(parent.context),
            R.layout.view_history_list_item,
            parent,
            false
        )
    ) : RecyclerView.ViewHolder(binding.root) {

        fun bind(item: ListViewModel.ListItem, isActivated: Boolean = false) {
            binding.model = item
            itemView.isActivated = isActivated

            val imageView = itemView.findViewById<AppCompatImageView>(R.id.history_item_image)

            if(itemView.isActivated) {
                val parameter = imageView?.layoutParams as ConstraintLayout.LayoutParams
                parameter.setMargins(
                    parent.context.resources.getDimension(R.dimen.spacing_small).toInt(),
                    parent.context.resources.getDimension(R.dimen.spacing_small).toInt(),
                    parent.context.resources.getDimension(R.dimen.spacing_small).toInt(),
                    parent.context.resources.getDimension(R.dimen.spacing_small).toInt()
                )

                imageView.layoutParams = parameter
            } else {
                val parameter = imageView?.layoutParams as ConstraintLayout.LayoutParams
                parameter.setMargins(0,0,0,0)
                imageView.layoutParams = parameter
            }
        }

        fun getItemDetails(): ItemDetailsLookup.ItemDetails<Long> =
            object : ItemDetailsLookup.ItemDetails<Long>() {
                override fun getPosition(): Int = adapterPosition
                override fun getSelectionKey(): Long? = itemId
            }
    }

    class HistoryGroupHeaderViewHolder(
        private val parent: ViewGroup,
        private val binding: at.app.databinding.ViewHistoryListGroupHeaderItemBinding = DataBindingUtil.inflate(
            LayoutInflater.from(parent.context),
            R.layout.view_history_list_group_header_item,
            parent,
            false
        )
    ) : RecyclerView.ViewHolder(binding.root) {

        fun bind(item: ListViewModel.ListSectionHeader) {
            binding.model = item
        }
    }

    class EmptyViewHolder(
        private val parent: ViewGroup, view: View = LayoutInflater.from(parent.context).inflate(
            R.layout.view_history_empty_item,
            parent,
            false
        )
    ) : RecyclerView.ViewHolder(view)

    companion object {
        const val EMPTY = 0
        const val ITEM = 1
        const val HEADER = 2
    }
}


class MyItemDetailsLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup<Long>() {
    private val log = LoggerFactory.getLogger(ListAdapter::class.java)

    override fun getItemDetails(e: MotionEvent): ItemDetails<Long>? {
        val view = recyclerView.findChildViewUnder(e.x, e.y)
        if (view != null) {
            return try {
                if(recyclerView.getChildViewHolder(view) is ListAdapter.HistoryViewHolder) {
                    (recyclerView.getChildViewHolder(view) as ListAdapter.HistoryViewHolder)
                        .getItemDetails()
                } else {
                    null
                }
            } catch (ex: Exception) {
                log.error("Error on getItemDetails. ", ex)
                null
            }
        }
        return null
    }
}

data class ListAdapterItem<out T>(val value: T, val viewType: Int)

And this is my layout:

<?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools">
    
        <data>
            <import type="android.view.View" />
    
            <variable
                name="viewModel"
                type="at.app.ui.viewmodel.ListViewModel" />
        </data>
    
        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <include
                android:id="@+id/list_app_bar"
                layout="@layout/layout_toolbar" />
    
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/history_recycler_view"
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:background="@android:color/transparent"
                android:scrollbars="vertical"
                app:data="@{viewModel.items}"
                app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/list_app_bar" />
    
        </androidx.constraintlayout.widget.ConstraintLayout>
    </layout>

Upvotes: 0

Views: 509

Answers (1)

ashakirov
ashakirov

Reputation: 12360

When i add two items and the header, the header and one item are displayed.

problem is in your getItemCount method.

override fun getItemCount(): Int {
        return if (items.isEmpty()) 1 else items.filter { it.viewType == ITEM }.size
}

If you want to show 1 header and 2 elements that means that there are must be 3 items in recyclerview, so getItemCount must return 3. But now it looks like getItemCount will return 2, thats why recycerlview doesn't even create third element.

Upvotes: 1

Related Questions