Update recycler view content without refreshing the data.

Miguel Sesma
4 min readJan 15, 2019

Avoid graphical glitches by modifying specific ViewHolder items without redrawing them.

Showing information in a list, or a set of cards is probably the most commonly used graphical widget in any app. The process we usually follow is:

  • Create a model class with all the information needed in the view.
  • Fill a list of those model classes with the data to be shown and pass it to the RecyclerView adapter.
  • If something changes, we update that list and tell the adapter to calculate the differences (DiffUtil) and show the new data.

DiffUtil is a great improvement over the old notifyDataSetChanged method that forced the layout manager to redraw all the visible items. With DiffUtil only the changed items need to be redrawn. But what if all the items in a list need a small change?

Sometimes we need to update the content of the RecyclerView cells, the ViewHolders, but we don’t want to add more information to our model or refresh the information in the adapter and do an expensive DiffUtil calculation. If the change affects several cells, it can cause the whole list flicker.

In the above gif, a surgeon searches for ‘knee’ related surgical procedures in our Touch surgery app. As the user writes, the text is highlighted in every displayed row. This is the simplest case: every item must receive the same information. this effect can be implemented with an observable that every row can observe, so we won’t need specific data for each item and the list can be fast scrolled and every title will still show the correct search highlight because even recycled rows will be subscribed to the observable.

ViewHolder:

// Code in the ViewHolder that subscribes to the observable
override fun setSearchObservable(searchObservable: Observable<String>): Disposable {
return searchObservable
.debounce(SEARCH_TYPING_INTERVAL, TimeUnit.MILLISECONDS)
.distinctUntilChanged()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { text ->
searchText = text
setTitle()
}
}

And the adapter:

//code in the adapter that sets the observable at View Holder creation time
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val viewHolder: ProcedureViewHolder = .....

compositeDisposable.add(viewHolder.setSearchObservable(searchDecorator.subjectText))
return viewHolder
}

With only a few lines of code, the ViewHolder observes the search text, unnecessary updates are prevented with debounce and distinct operators, and calls the setTitle() method. The code in the adapter is even simpler: in the onCreateViewHolder() method the observable is set and the disposable is added to a CompositeDisposable.

This system works perfectly when we need to send the same information to every row, but it does not scale to sending different data to each row or only a few of them.

Let’s create a progress indicator for our items. We will need to change the indicator icon and update the progress bar with a percentage value. This percentage can be different for each item, of course.

RecyclerView adapter has overloads of notifyItemChanged and notifyItemRangeChanged methods with a payload: Object parameter that will allow us to pass an object to one or several items without completely redrawing them.

Both notifyItemChanged and notifyItemRangeChanged adapter methods are item change events, not structural change events. This means that if the modified item(s) are already bound to a ViewHolder, the updated item will be bound to the same ViewHolder that is already painted. So we only need to set a specific value for the element in the item we want to change.

ViewHolder:

// Code in the view Holder
fun bind(model: phaseModel){
...// Draw the whole item
}

fun setProgress(value: Int){
procedure_download_icon.setProgress(value)
}

And the adapter:

// Code in the adapter
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List<Any>) {
when {
payloads.isEmpty() -> holder.bind(models[position]
else -> holder.setprogress(payloads[0] as Int)
}
}

fun setProgress(progresses: List<Int>) {
progresses.forEachIndexed { i, _ ->
notifyItemChanged(i, it)
}
}

Calling notifyItemChanged and notifyItemRangeChanged with a payload parameter, a progress percentage in the example, will cause onBindViewHolder to be called for that item with a list of all the payloads set for that item. We can select to draw the whole item or only update an element of it depending on this list being empty. Passing a null payload to notifyItemChanged or notifyItemRangeChanged will clear all payloads for that item.

This technique can be used for multiple purposes like selecting or expanding items or showing animations inside a card.

--

--

Miguel Sesma

Principal Android Developer in the healthcare industry.