Notify RecyclerView On a Specific Item Update

Occasionally when working with RecyclerView, we want to be able to notify on a specific update. We want to know the item position in the list, as well as which update was it (insert, remove, change). Maybe there’s some more information we want to pass about this update. For example, a reason for the change, or the originated screen, or the date it occurred at.

Knowing the specific change will allow us to call notifyItemAdded()for instance, instead of notifyItemSetChanged(), which will go over the entire list.

To be honest, using the Paging Library or just DiffUtil, very often does the trick and the performance are mostly great with it. However, for some cases, it can still be redundant work we would want to save.

How to notify a change in the list?

LiveData is a great tool to observe and notify changes in the UI. It wraps an object, and whenever this object, changes, LiveData notifies all its observers on the new value. However, if we’d wrap a list of items in LiveData, and an item in the list changes — the list object itself stayed the same. Meaning, if we still want the observers to be notified about an item change- we’d need so small trick to sort this out.

Also, as mentioned, we’d want to keep some info about the change and pass it to the observers. How should we do that?

To address that, here’s a recipe I’ve used a few times:

This specific example is built on top of a sample I’ve done. It allows the user to choose the actions to execute during a phone call (text to speech, record, etc..) and uses Nexmo Voice API to execute them. I’ve written some more about it on a different post.

This is just a demo project, obviously, but this idea has been useful for me in more than one complex app.

Setting the Presentation Layer

On a previous post series I shared the architecture I like to apply when building an app, with Presentation, Domain and Data layers.

My Presentation layer holds a UIModel, which extends ViewModel. Its role is to give the Presentation the data it needs to present in the simplest way, so that there are as minimal computations and logic as possible.

In this case, the UiModel holds:

  • the phone number to call to
  • the phone number to call from
  • the list of actions to execute

Anytime there’s a change in the actions list, we want to notify the observers, in this case, to notify the UI and the RecyclerView Adapter.

This is my UI Model:

class CallRequestUiModel : ViewModel() {
lateinit var toPhone: String
lateinit var fromPhone: String
val actions = ListLiveData<NccoAction>()
}

What is ListLiveData ?

As mentioned, we’d need to add some more info and logic to LiveData. Therefore, I extended it.

First, it wraps a value of type ListHolder, to hold on to all the info we need, not just the list of items:

class ListLiveData<T> : LiveData<ListHolder<T>>() {
}

In this case, ListHolder holds the list of items, the index of the changed item, and the type of the update:

data class ListHolder<T>(val list: MutableList<T> = mutableListOf()) {
var indexChanged: Int = -1
var updateType: UpdateType? = null
}

UpdateType is an enum I created, that matches the changes a RecyclerView can do, so that we’d be able to notify on a specific change:

private enum class UpdateType {
INSERT, REMOVE, CHANGE
}

Since UpdateType job will be to determine which method are we going to call on the Adapter, I used the cool Kotlin feature that allows you to implement interfaces using enums.

Note: This is a feature that I like in Kotlin. BUT! make sure to not abuse it. For me, it’s important to not mix the architecture layers. Since in this case, both the ListHolder (which owns the UpdateType) and the action it does (notifying the adapter) belong to the Presentation layer I’m happy to implement it this way. For example, if an enum that belongs to the UI will implement methods that use the Data layer, I’d not recommend it as much.

private enum class UpdateType {
   abstract fun notifyChange(
adapter: RecyclerView.Adapter<RecyclerView.ViewHolder>,
indexChanged: Int
)
    INSERT {
override fun notifyChange(
adapter: RecyclerView.Adapter<RecyclerView.ViewHolder>,
indexChanged: Int) = adapter.notifyItemInserted(indexChanged)
},

REMOVE {
override fun notifyChange(
adapter: RecyclerView.Adapter<RecyclerView.ViewHolder>,
indexChanged: Int) = adapter.notifyItemRemoved(indexChanged)
},
    CHANGE {
override fun notifyChange(
adapter: RecyclerView.Adapter<RecyclerView.ViewHolder>,
indexChanged: Int) = adapter.notifyItemChanged(indexChanged)
};

}

This enum is private, to help with separating the layers as I mentioned above. This way, only ListHolder, which is the changed item, will be able to apply it:

data class ListHolder<T>(val list: MutableList<T> = mutableListOf()) {
var indexChanged: Int = -1
private var updateType: UpdateType? = null

//...

fun applyChange(adapter: RecyclerView.Adapter<RecyclerView.ViewHolder>) {
updateType?.notifyChange(adapter, indexChanged)
}

Notifying the Observer on Change

In my MainActivity.onCreate() (this is the starting point for my Presentation layer component), I ask to observe the changes in the list of actions:

uiModel.actions.observe(this, this)

For a simple example, my MainActivity also implements Observer and so, overrides onChanged(). Whenever an item in actions updates, we’d just ask the new value of LiveData(the ListHolder) to apply the change on the adapter. The value,ListHolder, already knows the exact item that was updated, and what to apply on the adapter:

override fun onChanged(value: ListHolder<NccoAction>?) {
list.adapter?.let {
value?.applyChange(it)
}
}

What does updating an item looks like?

The UI can call addItem(), removeItemAt() or setItem(), which is actually transferred to be handled by ListLiveData:

For example, the UI calls:

fun addAction(action: NccoAction) {
uiModel.actions.addItem(action)
}

ListHolder transfers the request to perform the update to ListHolder, and then notifies the observers (here- the UI ) on the update.

class ListLiveData<T> : LiveData<ListHolder<T>>() {

//...

fun addItem(item: T, position: Int = value?.size() ?: 0) {
value?.addItem(position, item)
value = value

}

fun removeItemAt(position: Int) {
value?.removeItemAt(position)
value = value
}


fun setItem(position: Int, item: T) {
value?.setItem(position, item)
value = value
}


}

Why is value = value?

There’s probably a more elegant way to do this 🤷‍♀ ️ As mentioned, since I don’t update the value itself (the ListHolder object), but only its content, the update will not dispatch as a value change to the observers. LiveData dispatches the change to the observers only when setValue() is called. So I tricked it by using setValue() with the same value 😬

From source code of LiveData.java
@MainThread
protected void setValue(T value) {
assertMainThread("setValue");
mVersion++;
mData = value;
dispatchingValue(null);
}

Another note about ListLiveData:

For convenience, in order to make it behave more list a List, I implemented 2 methods:

class ListLiveData<T> : LiveData<ListHolder<T>>() {

val size: Int get() {
return value?.size() ?: -1
}
    operator fun get(position: Int): T? {
return value?.list?.get(position)
}

}

size(): is used for example on the Adapter.getItemCount()

override fun getItemCount(): Int {
return actions.size
}

operator fun get() is to get a specific item from the list, for example, on Adapter.onBindViewHolder():

override fun onBindViewHolder(holder: NccoCompVH, index: Int) {
actions[index]?.let { holder.bind(it) }
}

Let’s see the entire flow

  1. UI asks ListLiveData to update a list item →
  2. ListLiveData asks ListHolder to keep the update info →
  3. ListLiveData sets its value and dispatches the change to the Observers
  4. Observers (UI) onChange() is called with the new value, which is on type ListHolder
  5. UI asks the new value, ListHolder, to apply the update it holds the data of
  6. UpdateType, which is contained in ListHolder notifies the adapter on the update
  7. the Adapter applies the specific item update (as well as you can pass more info and do whatever you want with it!)
  8. everyone’s happy!!

That’s it!

That’s it, hope it helps :)

Here’s the code for this sample on Github

Happy coding! Thank you for reading. Here (on Twitter actually) for questions 🙌🙏😍✌