android

Interaction with RecyclerView using Kotlin DSL

Few days ago I finished a project for one client – it was typical MVP project, where everything was changing quite often and deadline was pretty close. At some point, I had to stop for a moment and refactor few places that were changing the most. One of them was the interaction with RecyclerView.


The problem


Normally when I interact with RecyclerView item, I am passing lambda function via RecyclerView.Adapter constructor, in onBindViewHolder onCreateViewHolder* I am setting the click listener for example on button, and inside this click listener I am invoking the lambda function from constructor.

*I was doing it in onBindViewHolder but after discussion on /r/androiddev on Reddit under this link , watchful eyes of fellow developers pointed me more performant approach. Thanks!

class MeetingsAdapter(
    private val meetings: List<Meeting>,
    private val onJoinButtonClicked: (String) -> Unit,
    private val onAttachmentIconClicked: (String) -> Unit,
    private val onLocationIconClicked: (Coordinates) -> Unit,
    private val onShareIconClicked: (String) -> Unit
): RecyclerView.Adapter<MeetingViewHolder>() {

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

    override fun getItemCount() = meetings.size

    override fun onBindViewHolder(holder: MeetingViewHolder, position: Int) {
        holder.bind(meetings[holder.adapterPosition])
    }
}
class MeetingViewHolder(
    view: View,
    onJoinButtonClicked: (String) -> Unit,
    onAttachmentIconClicked: (String) -> Unit,
    onLocationIconClicked: (Coordinates) -> Unit,
    onShareIconClicked: (String) -> Unit
): RecyclerView.ViewHolder(view) {

    private var meeting: Meeting? = null

    init {
        with(view) {
            btn_join.setOnClickListener {
                onJoinButtonClicked(meeting?.meetingUrl)
            }
            ic_attachments.setOnClickListener {
                onAttachmentIconClicked(meeting?.attachmentsUrl)
            }
            ic_location.setOnClickListener {
                onLocationIconClicked(meeting?.coordinates)
            }
            ic_share.setOnClickListener {
                onShareIconClicked(meeting?.shareUrl)
            }
        }
    }

    fun bind(payload: Meeting) {
        this.meeting = payload
        with(itemView) {
            meeting_title.text = payload.name
            meeting_date.text = payload.date
        }
    }
}
val adapter = MeetingsAdapter(MeetingsProvider.meetings,
    onJoinButtonClicked = {
        // join meeting action
    },
    onAttachmentIconClicked = {
        // show attachments action
    },
    onAttendeeClicked = {
        // show attendee action
    },
    onLocationIconClicked = {
        // navigate to location action
    },
    onShareIconClicked = {
        // share the meeting action
    })
recyclerview.adapter = adapter



The solution


In this project, when passing lambda functions to different ViewHolders started to be annoying, I came up with the solution below:

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

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

    override fun getItemCount() = meetings.size

    override fun onBindViewHolder(holder: MeetingViewHolder, position: Int) {
        holder.bind(meetings[holder.adapterPosition])
    }
}
fun meetingInteractor(block: MeetingInteractor.() -> Unit) = MeetingInteractor().apply(block)

class MeetingInteractor {
    var onJoinButtonClicked: (String) -> Unit = {}
    var onAttachmentIconClicked: (String) -> Unit = {}
    var onLocationIconClicked: (Coordinates) -> Unit = {}
    var onShareIconClicked: (String) -> Unit = {}
    var onAttendeeClicked: (Attendee) -> Unit = {}

    fun onJoinButtonClicked(listener: (String) -> Unit) {
        onJoinButtonClicked = listener
    }

    fun onAttachmentIconClicked(listener: (String) -> Unit) {
        onAttachmentIconClicked = listener
    }

    fun onLocationIconClicked(listener: (Coordinates) -> Unit) {
        onLocationIconClicked = listener
    }

    fun onShareIconClicked(listener: (String) -> Unit) {
        onShareIconClicked = listener
    }

    fun onAttendeeClicked(listener: (Attendee) -> Unit) {
        onAttendeeClicked = listener
    }
}

val adapter = MeetingsAdapter(MeetingsProvider.meetings,
    meetingInteractor {
        onJoinButtonClicked {
            // join meeting action
        }
        onAttachmentIconClicked {
            // show attachments action
        }
        onAttendeeClicked {
            // show attendee action
        }
        onLocationIconClicked {
            // navigate to location action
        }
        onShareIconClicked {
            // share the meeting action
        }
    })
recyclerview.adapter = adapter
class MeetingViewHolder(
    view: View,
    meetingInteractor: MeetingInteractor
): RecyclerView.ViewHolder(view) {

    private var meeting: Meeting? = null

    init {
        with(view) {
            btn_join.setOnClickListener {
                meetingInteractor.onJoinButtonClicked(meeting?.meetingUrl)
            }
            ic_attachments.setOnClickListener {
                meetingInteractor.onAttachmentIconClicked(meeting?.attachmentsUrl)
            }
            ic_location.setOnClickListener {
                meetingInteractor.onLocationIconClicked(meeting?.coordinates)
            }
            ic_share.setOnClickListener {
                meetingInteractor.onShareIconClicked(meeting?.shareUrl)
            }
        }
    }

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



Conclusion


What is the biggest benefit of this approach? Having all of the definitions in one place. If you need to change which object listener should pass, you just need to change the definition in MeetingInteractor class, the implementation in MeetingViewHolder and place where you’re invoking the lambda function. With lots of different ViewHolders, you will either need to change every constructor that has this lambda as an argument or you will probably have everything in onCreateViewHolder which is an easy way to have a big and messy RecyclerView.Adapter class.

Another thing is that you just have the codebase much cleaner, you have separate class for interactions, the codebase is much smaller, it’s easier to maintain in long term and you don’t need to worry about passing every lambda function to each ViewHolder.

Thanks for reading!

Mariusz Brona

Dodaj komentarz

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