RecycleView Adapter with Click Listener
April 17, 2020
There are many ways to write a RecyclerView Adapter depending on the amount of the data it can handle, how often the data gets updated, etc. Lately, I’ve been using the following approach for basic use cases where Paging Library is not needed:
Adapter
class RepoAdapter(private val clickListener: (RepoOwner) -> Unit) : ListAdapter<Repo, RepoAdapter.RepoViewHolder>(RepoDiffCallback()) { override fun onCreateViewHolder(parent: ViewGroup, position: Int): RepoViewHolder { val inflater = LayoutInflater.from(parent.context) val binding = RepoItemBinding.inflate(inflater, parent, false) return RepoViewHolder(binding, clickListener) } override fun onBindViewHolder(holder: RepoViewHolder, position: Int) { holder.bind(getItem(position)) } } class RepoViewHolder( private val binding: RepoItemBinding, private val clickListener: (RepoOwner) -> Unit ) : RecyclerView.ViewHolder(binding.root), View.OnClickListener { private lateinit var item: Repo init { binding.repoOwner.setOnClickListener(this) } fun bind(item: Repo) { this.item = item binding.repoName.text = item.name binding.repoOwner.apply { text = item.owner.login } override fun onClick(v: View?) { v?.let { clickListener.invoke(item.owner) } } } class RepoDiffCallback : DiffUtil.ItemCallback<Repo>()
First, I am using a ListAdapter and a DiffCallback to avoid re-rendering all items in the Adapter when items are added, updated or removed.
Second, notice the click listener is passed into the Adapter’s constructor. The custom View Holder could be made an inner class
so that it has access to the click listener but personally I think too much nesting makes the code less readable. Constructor “injection” (passing the click listener in the constructor to the ViewHolder gives us a blueprint what the ViewHolder is about.
Fragment
class ReposFragment : Fragment() { private val recyclerViewAdapter = RepoAdapter override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.reposRecyclerView.adapter = recyclerViewAdapter viewModel.repos.observe(viewLifecycleOwner, Observer ) viewModel.lookupRepos() } private fun onRepoOwnerClicked(repoOwner: RepoOwner) { val action = ReposForQueryFragmentDirections .actionReposToUserDetails(repoOwner.login) findNavController().navigate(action) } }
As you see, I am creating an Adapter and passing a click listener (lambda that calls onRepoOwnerClicked(owner)
) into it. The actual click will be handled by the Fragment that created it.
This way all navigation, tracking, etc is done in a consistent manner—in Fragments themselves.
You can see the full source code including the navigation component setup here.