android

Dynamic views in RecyclerView ViewHolders

In this blogpost I want to show you a quick and nice solution for handling dynamic views in ViewHolders. In my opinion this particular approach is a good way to have performant and easy to maintain code for the RecyclerView.

Important thing in RecyclerView

When we work with RecyclerView, we need to always remember how RecyclerView works underneath. Basically, RecyclerView is based on the concept of ‘Virtualized List’. It is a technique, where you have tons of items to display on the list, but you’re only rendering a few of them and reuse them for other items. That’s basically what RecyclerView does – if you have let’s say 10000 items on the list, only few ViewHolders will be created, and each item on the list can be bind to different ViewHolder every time you scroll the list. That’s why RecyclerView can be (and when created properly – is) so performant. How to make sure that you’ve done right job working with RecyclerView?

  • The data you pass to the RecyclerView is immutable. That’s it. You can’t change the state of the model in onBindViewHolder method, because it will only last until the view is recycled and ViewHolder is used for another item on the list
  • You can’t do too much work in onBindViewHolder method. The best you can do is just actually do what the method describes – bind the data to the view. You should just set your text, images etc. and that’s it.
  • If you want to display different ViewHolders in the RecyclerView – assign each ViewHolder its ViewType. Don’t do some weird stuff in onBindViewHolder, just create another ViewHolder for different view.

But sometimes those general rules don’t apply. Some time ago I’ve had this situation, where I had to display dynamic amount of views in each ViewHolder.

The problem

Each item on the list had 0 to 12 avatars to display. There were few ways to achieve that:

  • Create different ViewHolder for each amount of avatars, where each ViewHolder has corresponding amount of ImageViews and we use if statement to map each avatar to corresponding ImageView in the layout. (that’s a joke, relax)
  • Create XML with 12 ImageViews and add some logic in onBindViewHolder to show/hide specific amount of avatars, depending on the model. At least changing visibility seems to be enough performant, but I don’t think it’s a good idea in terms of maintenance.
  • Create ImageViews dynamically and add them to the layout. That’s a good lead, however I can’t imagine how the performance would drop if I create the views in onBindViewHolder.
  • The last idea, and the one I want to show – create ImageViews pool which I will reuse to show different amount of avatars

The solution

As you might remember, only few ViewHolders are created, even if you have hundreds of items on the list. So my idea was to create pool of ImageViews in ViewHolder’s constructor. Thanks to that, I was able to easily switch to my custom ImageView where I do additional drawings. And I did it only in one place – I didn’t have to change lots of ImageViews in XML for example.

Then, when we bind the model to the ViewHolder, we will just take the needed amount of ImageViews from the pool and add them to the LinearLayout, which will display them in order. There is just one thing to remember – if we call remove() on an empty LinkedList, the app will crash. It shouldn’t happen if we watch the max count of avatars, but it’s better to just create new ImageView if there are no more to reuse – the ImageView created in bind() method can still be reused later.

private const val MAX_ATTENDEES_VISIBLE = 12
class MeetingViewHolder(view: View): RecyclerView.ViewHolder(view) {

    private val attendeeImageViewPool = LinkedList<ImageView>()

    init {
        for(i in 0..MAX_ATTENDEES_VISIBLE) {
            attendeeImageViewPool.add(AttendeeImageView(view.context))
        }
    }

    fun bind(payload: Meeting) {
        with(itemView) {
            meeting_title.text = payload.name
            meeting_date.text = payload.date
        }
        setupAttendees(payload.attendees)
    }

    private fun setupAttendees(attendees: List<Attendee>) {
        with(itemView) {
            layout_attendees.removeAllViews()
            if (attendees.isEmpty()) {
                no_attendants.visibility = View.VISIBLE
                layout_attendees.visibility = View.GONE
            } else {
                no_attendants.visibility = View.GONE
                layout_attendees.visibility = View.VISIBLE
                attendees.forEach { attendee ->
                    val attendeeImageView = 
                        if (attendeeImageViewPool.size > 0) 
                            attendeeImageViewPool.remove() 
                        else 
                            AttendeeImageView(this.context)
                    attendeeImageView.setImageDrawable(attendee.initialsDrawable)
                    layout_attendees.addView(attendeeImageView)
                }
            }
        }
    }

    fun onViewRecycled() {
        val images = itemView.layout_attendees.children as Sequence<AttendeeImageView>
        attendeeImageViewPool.addAll(images.toList())
    }
}

You can see that in the bottom of the file, there is a function called onViewRecycled(). This function is called from MeetingsAdapter when the view is recycled. In this method we are adding the ImageViews back to the pool, so they can be reused later:

class MeetingsAdapter(
    private val meetings: List<Meeting>
): RecyclerView.Adapter<MeetingViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int)
        = MeetingViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.recycler_item_meeting, parent, false))

    override fun getItemCount() = meetings.size

    override fun onBindViewHolder(holder: MeetingViewHolder, position: Int) {
        holder.bind(meetings[holder.adapterPosition])
    }
    
    override fun onViewRecycled(holder: MeetingViewHolder) {
        super.onViewRecycled(holder)
        holder.onViewRecycled()
    }
}

Conclusion

When I deal with RecyclerView, I always try to use immutable data and just bind the payload to the views. However sometimes it is unachievable and we have to do some tricks and keep in mind the performance, because often we are unable to tell how much objects are going to be displayed in the list. I think this solution is a nice compromise between the amount of the code we have to maintain and performance we need to take care of.

Cheers!

Mariusz Brona

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *