
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 onBindViewHolderonCreateViewHolder
* 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!