Daily Dispatch 22 - ListAdapter to the Rescue
It’s been a while since I’ve written about some “normal” Android Development.
Today I followed a lead on DiffUtil, a library that a senior developer told me about. It is supposed to be used in Recycler views with adapters to avoid having to call notifyAllDataChanged() and having some uneasy visual bugs.
So I looked into it and found some lovely blog posts on the matter.
One that I found really enlightening was this one.
So I went back to my MovieApp to fix my visual bugs.
This is the old way of using RecyclerView.Adapter:
class MovieListAdapter(private var moviesList: ArrayList) : RecyclerView.Adapter<MovieListAdapter.MovieViewHolder>(), MovieClickListener
{
private lateinit var accessView : MovieItemBinding
@SuppressLint("NotifyDataSetChanged")
fun updateMoviesList(newMoviesList: List) {
moviesList.clear()
moviesList.addAll(newMoviesList)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = DataBindingUtil.inflate(inflater, R.layout.movie_item, parent, false)
accessView = view
return MovieViewHolder(view)
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) { val item = moviesList[position]
accessView.movie = item
accessView.listener = this
}
override fun onMovieClicked(v: View) {
val id = v.movieId.text.toString()
val action = MoviesListFragmentDirections.actionMovieFragmentToMovieDetail()
action.movieId = id.toInt()
findNavController(v).navigate(action)
}
override fun getItemCount(): Int = moviesList.size
inner class MovieViewHolder(binding: MovieItemBinding) : RecyclerView.ViewHolder(binding.root) {
var movie: Movie? = binding.movie
var listener: MovieClickListener? = binding.listener
}
}
Now there was a lot going on here. But the worst offender was the updateMoviesList calling notifyDataSetChanged
. What this does is forces the UI to re-render the whole view, even on items that haven’t changed from the previous movie list.
So what was the solution? First I implemented this Callback class, that uses DiffUtil. Which is basically a class that facilitates comparison between items. And more importantly, will be used later on the new Adapter.
class MovieListDiffCallback : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return oldItem == newItem
}
}
After this was done, I went to restructure my MovieListAdapter class, so it inherits from ListAdapter, which is a improved version of dealing with lists. Less boilerplate code and integration with DiffUtil.
Here’s the new declaration:
class MovieListAdapter(
) : ListAdapter<Movie, MovieListAdapter.MovieViewHolder>(MovieListDiffCallback()) {
...
inner class MovieViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
...
}
}
As you can see I took some liberty with MovieViewHolder as well, switched from binding to view. I decided to start using viewBinding instead of databinding to facilitate my life later on.
Here’s the new functioning code for the MovieListAdapter:
class MovieListAdapter(
) : ListAdapter<Movie, MovieListAdapter.MovieViewHolder>(MovieListDiffCallback()) {
lateinit var view : View
private fun clickListener (v: View) {
val id = v.movieId.text.toString()
val action = MoviesListFragmentDirections.actionMovieFragmentToMovieDetail()
action.movieId = id.toInt()
findNavController(v).navigate(action)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = MovieItemBinding.inflate(inflater, parent, false)
val view = binding.root
return MovieViewHolder(view)
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
holder.bind(getItem(position)) { newView -> clickListener(newView) }
}
inner class MovieViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(newMovie: Movie, clickListener: (View) -> Unit) {
itemView.apply {
loadImage(imageView, newMovie.posterPath)
movieId.text = newMovie.id.toString()
title.text = newMovie.title
releaseDate.text = newMovie.releaseDate
setOnClickListener(clickListener)
}
}
}
}
There’s only one subtle detail about calling the movieListAdapter to update the list, we need to change any old invocations of adapter.updateMoviesList(newList) to adapter.submitList(newList).
And that is it for today, see you tomorrow (whenever that is 😉 ).