Let's say that we want to list all the downloading files with progress (this is just a sample to show the problem).
Each item could be represented by this simple class:
data class DownloadingFileItem(val url: String, val progress: Int)
Now as you can see progress is a constant value. Whenever the download progress has been changed, we would have to reload our RecyclerView
or at least one particular ViewHolder
that represent our item. Of course this would be quite easy task with notifyItemChanged()
or DiffUtil
, but let's make problem more complicated and assume that rebinding may produce some unwanted view changes (like stopping started animation, etc.) or changes are very frequent. So the only solution would be to make our item more reactive and change constant progress to LiveData
or Observable
object. But how to achieve that?
You could achieve something like that by keeping a map of ViewHolder
and stable id in your adapter. When you want to update a particular item, use that id to find the viewHolder and update the view.
data class Item(val id: String, val progress: Int)
A viewHolder that keeps the reference to the id which we can use to remove it from the map once it's recycled.
class ObservableViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val seek = itemView.findViewById<SeekBar>(R.id.seek)
private val num = itemView.findViewById<TextView>(R.id.num)
var id: String? = null
fun bind(item: Item) {
id = item.id
num.text = item.id
seek.progress = item.progress
}
}
Your adapter
class ObservableAdapter: RecyclerView.Adapter<ObservableViewHolder>() {
private var items = listOf<Item>()
private val viewHolderMap = hashMapOf<String, ObservableViewHolder>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ObservableViewHolder {
return ObservableViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false))
}
override fun getItemCount(): Int {
return items.size
}
override fun onBindViewHolder(holder: ObservableViewHolder, position: Int) {
val item = items[position]
holder.bind(item)
viewHolderMap[item.id] = holder
}
override fun onViewRecycled(holder: ObservableViewHolder) {
super.onViewRecycled(holder)
holder.id?.let { viewHolderMap.remove(it) }
}
fun updateItems(items: List<Item>) {
this.items = items
}
fun update(item: Item) {
viewHolderMap[item.id]?.bind(item)
}
}
This is how my demo example is updating the view.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val adapter = ObservableAdapter()
var items = listOf(
Item("1", 10),
Item("2", 20),
Item("3", 30),
Item("4", 10),
Item("5", 20),
Item("6", 30),
Item("7", 10),
Item("8", 20),
Item("9", 30),
Item("10", 10),
Item("11", 20),
Item("12", 30),
Item("13", 10),
Item("14", 20),
Item("15", 30)
)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = adapter
adapter.updateItems(items)
adapter.notifyDataSetChanged()
findViewById<View>(R.id.cta).setOnClickListener {
val updated = items[0].copy(progress = items[0].progress + 10)
adapter.update(updated)
items = items.toMutableList().apply {
removeAt(0)
add(0, updated)
}
adapter.updateItems(items)
// No call to notifyDatasetChanged()
}
}
}
Here's how the demo looks
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With