Pratik Mhatre
Pratik Mhatre

Reputation: 779

Android Recyclerview notifyItemChanged(position ,payLoad) don't work if the view at that position is not visible on screen or scrolled out

I am using RecyclerView in my app, it has itemViewCache of 20 items because max possible size of my list would be 20. It is "Single Item Selection List". Now When the list loads, the first cell becomes highlighted as default selected position. If i click the cell at different position, the view at that position becomes selected & highlighted, simultaneously previously selected view becomes white ie unselected. Above works fine as I use adapter.notifyDataSetChanged(position, payLoad) method to update data in the list. But when the cell at currently selected position is scrolled out of the screen and I try to select new position, however the new cell grabs the highlighting and becomes selected, it does not makes previously selected cell white. For the cell at scrolled out position, system dont even call the notifyDataSetChanged(positioin, payLoad) method to make ui in it. Any help would be greatly appreciated as I work a lot with RecyclerViews.

Here is the code of my adapter :

class EventsCalendarAdapter(var eventSectionedFrag: EventSectionedFrag, var arrayList: ArrayList<String>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    val view = LayoutInflater.from(parent.context).inflate(R.layout.adapter_calendar_months, parent, false)
    return CalendarHolder(view)
}

override fun getItemCount() = arrayList.size


override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: MutableList<Any>) {
    if (payloads.isNotEmpty()) {
        var bundle = payloads[0] as Bundle
        var isSelected: Boolean? = bundle.getBoolean("selected")
        var setCurrentMonthSelected: Boolean? = bundle.getBoolean("setMonth")


        with(holder) {
            if (isSelected != null) {
                val tvMonth = itemView.findViewById<TextView>(R.id.tv_month)
                tvMonth.apply {
                    setTextColor(ContextCompat.getColor(eventSectionedFrag.activity!!, if (isSelected) R.color.white else R.color.better_than_black))
                    background.setColorFilter(ContextCompat.getColor(eventSectionedFrag.activity!!, if (isSelected) android.R.color.holo_red_light else R.color.white), PorterDuff.Mode.SRC)
                }
            }

            if (setCurrentMonthSelected != null) {
                val view = itemView.findViewById<View>(R.id.view_current)
                view.visibility = if (setCurrentMonthSelected) {
                    View.VISIBLE
                } else {
                    View.INVISIBLE
                }
            }
        }
    } else {
        super.onBindViewHolder(holder, position, payloads)
    }
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    val myHolder = holder as CalendarHolder
    myHolder.bindItems(eventSectionedFrag = eventSectionedFrag, month = arrayList[position], position = position)
}

class CalendarHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    fun bindItems(eventSectionedFrag: EventSectionedFrag, month: String, position: Int) {
        val tvMonth = itemView.findViewById<TextView>(R.id.tv_month)
        val cardMonth = itemView.findViewById<ConstraintLayout>(R.id.card_month)
        val view = itemView.findViewById<View>(R.id.view_current)
        view.visibility = View.INVISIBLE
        val callbacks = eventSectionedFrag as CalendarCallbacks

        tvMonth.text = month
        view.apply {
            visibility = if (callbacks.getCurrentMonthPosition() == position) {
                View.VISIBLE
            } else {
                View.INVISIBLE
            }
        }
        cardMonth.setOnClickListener {
            callbacks.onMonthSelected(adapterPosition, tvMonth)
        }
    }
}

fun addMonths(arrayList: ArrayList<String>) {
    this.arrayList.clear()
    this.arrayList.addAll(arrayList)
    notifyDataSetChanged()
}

interface CalendarCallbacks {
    fun onMonthSelected(position: Int, currentSelectedView: TextView)
    fun getCurrentMonthPosition(): Int
}

}

and here is how I am invoking the changes from my activity,

override fun notifyCalendar(isCurrentYearSelected: Boolean, position: Int) {
    var bundle = Bundle()
    bundle.putBoolean("setMonth", isCurrentYearSelected)
    calendarAdapter.notifyItemChanged(position, bundle)
}

Upvotes: 6

Views: 7511

Answers (2)

Hong Duan
Hong Duan

Reputation: 4304

From the RecyclerView.Adapter#notifyItemChanged(int position, java.lang.Object payload) docs:

Adapter should not assume that the payload will always be passed to onBindViewHolder(), e.g. when the view is not attached, the payload will be simply dropped.

So when the cell at currently selected position is scrolled out of the screen, nothing will change because the old selected view is not exist.

You can modify your arrayList to store the selected status in it, or add a method to CalendarCallbacks which can fetch the current selected position, but you must take care of changing the selected position when needed.

Upvotes: 3

Pawel
Pawel

Reputation: 17288

You're misunderstanding ViewHolder pattern and notifyItemChanged method.

ViewHolder - object holding the views, when you scroll through your RecyclerView, those objects are re-used to reduce memory and cpu consumption. Their purpose is to display the data set, it's generally unacceptable to hold any data within them.

notifyItemChanged - it's purpose is to explicitly update displayed ViewHolders. You have to modify underlying dataset (including array of selected items) before calling this method, so in case of future onBindViewHolder (with empty payloads) item is properly displayed.

Upvotes: 0

Related Questions