LukaG
LukaG

Reputation: 43

Android RecyclerView freezes UI on a notifyDataSetChanged call

I know there have been many questions regarding this topic, but I still could not find a solution.

I have an application, that loads and shows images (just like a regular gallery app on a smartphone)

I load all the images in the background with coroutines - no problem at this stage of the process.

The problem occurs when I notify my adapter about the data. notifyDataSetChanged() call freezes the UI for a couple of moments.

We are talking about relatively large grids (1000 + photos for example)

Almost all the answers in other threads suggest, that I should check for which items changed and call notifyItemChanged(index) rather than call notifyDataSetChanged() or use DiffUtil or something similar. I understand all of this, but at the RecyclerView initialization (when the application loads) there is no existing data, so I don't see other options as calling notifyDataSetChanged().

Adapter code:

class PhotosAdapter(private val mFragment: PhotosFragment, private val mListener: ClickListener) : RecyclerView.Adapter<PhotoViewHolder>() {

    private var photos: List<PhotoFile>? = null
    
    init {
        mFragment.mPhotosViewModel.photos.observe(mFragment, Observer {
            it?.let {
                photos = it
                notifyDataSetChanged()
            }
        })
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoViewHolder {
        return PhotoViewHolder.from(parent)
    }

    override fun onBindViewHolder(holder: PhotoViewHolder, position: Int) = holder.bind(
        position, photos!!.get(position), mFragment, mListener
    )

    override fun getItemCount(): Int = photos?.size ?: 0
}

class PhotoViewHolder private constructor(private val mBinding: PhotoGridItemBinding) : RecyclerView.ViewHolder(mBinding.root) {

    companion object {
        fun from(parent: ViewGroup) : PhotoViewHolder {
            val layoutInflater = LayoutInflater.from(parent.context)
            val binding = PhotoGridItemBinding.inflate(layoutInflater, parent, false)
            return PhotoViewHolder(binding)
        }
    }


    fun bind(index: Int, photo: PhotoFile, photosFragment: PhotosFragment, listener: ClickListener) {
        mBinding.index = index
        mBinding.photo = photo
        mBinding.viewHolder = this
        mBinding.listener = listener
        
        Glide.with(photosFragment).load(photo.uri)
            .thumbnail(0.1f).into(mBinding.photoGridItemThumbnail)

        mBinding.executePendingBindings()
    }
}

Regards, L

Upvotes: 0

Views: 2121

Answers (1)

Martin Marconcini
Martin Marconcini

Reputation: 27226

I believe your architecture is missing the final bit.

You said:

"at the RecyclerView initialization (when the application loads) there is no existing data, so I don't see other options as calling notifyDataSetChanged()"

I'm not 100% sure I understand what you mean, but I believe what you're experiencing is:

  1. The app starts, you set up your Adapter/RecyclerView but still have no data; you also observe some LiveData<List<T>> or similar from your ViewModel.
  2. You request data to your ViewModel, which is suspending one way or another, and asynchronously delivering a List<T> with your adapter's data.
  3. Once you receive a List<T> from your adapter, you set it via some method you added like adapter.setData(list) and then call adapter.notifyDataSetChanged().

And this takes forever.

The reason why other threads were recommending you use DiffUtil() (or its AsyncDiffUtil version) is because this helps the Adapter with the ... well.. adapting of your data to a View (holder), by calculating what needs to be adapter and what is the same.

We have not seen (so far) your Adapter, but my Guess is you're extending RecyclerView.Adapter<T>. This means you ought to do your own DiffUtil (or Async).

Alternatively, You can instead extend ListAdapter<T, K> where T is your data type (List<XXX>) and K is your ViewHolder type, for which you can use RecyclerView.ViewHolder if I correctly recall (and then have your ViewHolders Extend that).

This list adapter forces you to supply a DiffUtil in the constructor.

just chechking for differences between two (empty) lists makes no sense

Not really, you're making a problem where the Framework is already offering you a solution. DiffUtil will not be slower (minus a few ms?) if the lists are empty, since it will have nothing to compare. But when there IS data, it will do the heavy lifting for you.

IN any case, until you post some of your code, we're all talking on thin air. :)

Upvotes: 1

Related Questions