Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make reactive item inside RecyclerView?

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?

like image 337
Nominalista Avatar asked May 30 '19 13:05

Nominalista


1 Answers

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

enter image description here

like image 105
Froyo Avatar answered Sep 24 '22 11:09

Froyo