Search code examples
androidandroid-recyclerviewpicasso

RecyclerView with Images blocking the UI


I have a recyclerview that loads a set of items which mainly display an image. I retrieve this items in the background, in batches of 100. I load the images using Picasso. Images are quite big, but I resize them using fit().

Whenever the screen is loaded or refreshed using SwipeRefreshLayout, the UI blocks for less than a second, but enough to be noticeable. If I dont load the images but put just the text, then the UI block does not happen.

I put logging lines on Picasso and on every refresh the 100 images are retrieved, but I would guess Picasso is working in a background thread?

Adapter:

@ActivityScope
class LimitableListAdapter @Inject constructor() : RecyclerView.Adapter<LimitableListAdapter.ViewHolder>() {

    private var events: MutableList<Event> = mutableListOf()
    private var itemClick: ((Event, View) -> Unit)? = null


    override fun onBindViewHolder(holder: ViewHolder, position: Int) {

        val binding : ItemVideoGridScoreBinding = holder.binding

        var viewModel = binding.viewModel
        val event = events[position]

        //Unbind old viewModel if we have one
        viewModel?.unbind()

        // Create new ViewModel, set it, and bind it
        viewModel = EventViewModel(event)
        binding.viewModel = viewModel
        viewModel.bind()


        holder.setClickListener(itemClick)
    }

    override fun getItemCount(): Int = events.size


    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = DataBindingUtil.inflate<ItemVideoGridScoreBinding>(
                LayoutInflater.from(parent.context),
                R.layout.item_video_grid_score,
                parent,
                false
        )
        return ViewHolder(binding)
    }


    fun updateEvents(events: List<Event>, stride: Int) {
        var size = this.events.size
        Timber.w("Updating with: " + events.joinToString(",", transform = { e -> e.id.toString() }))
        this.events = events.toMutableList()
        notifyDataSetChanged()
        /*if (size == 0) {
            Timber.w("branch 1")
            var mutableList = events.toMutableList()
            if(mutableList.size == 0)
                return
            mutableList.add(Event.mockEvent(stride))
            this.events.addAll(mutableList)
            notifyDataSetChanged()
        } else {
            if (size > 2) {
                Timber.w("branch 2.1")
                this.events.addAll(size - 1, events.toMutableList())
                notifyItemRangeChanged(size-1, events.size)
            }
            else {
                Timber.w("branch 2.2")
                this.events.addAll(size, events.toMutableList())
                notifyItemRangeChanged(size, events.size)
            }

        }*/
        Timber.i("New list is: " +  this.events.joinToString(",", transform = { e -> e.id.toString() }))
    }

    fun clearList(){
        this.events.clear()
        notifyDataSetChanged()
    }

    fun setClickListener(itemClick: ((Event, View) -> Unit)?) {
        this.itemClick = itemClick
    }


    class ViewHolder(val binding: ItemVideoGridScoreBinding) : RecyclerView.ViewHolder(binding.root) {
        fun setClickListener(callback: ((Event, View) -> Unit)?) {
            binding.viewModel.clicks().subscribe() {
                callback?.invoke(binding.viewModel.event, itemView)
            }
        }
    }

}

BindingUtils:

@BindingAdapter({"app:coverUrl"})
    public static void loadCover(ImageView view, String imageUrl) {

            Picasso p = Picasso.with(view
                    .getContext());
                    p.setIndicatorsEnabled(true);

                    p.load(imageUrl)
                    .fit()
                    .centerInside()
                    .error(R.drawable.ic_broken_image)
                    .into(view);
        }
    }

xml:

(...)
     <ImageView
                    android:id="@+id/event_cover"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:scaleType="centerCrop"
                    app:coverUrl="@{videoItem.cover}"
                    tools:src="@drawable/img"
                    />
(...)

Solution

  • Assuming you are not able to create thumbnails on the server side (which would be the easiest solution), my suggestion would be to go with one of the following

    1. Using a local drawable as a Placeholder. This will not block the UI thread and the image can load in the background. Behaviour will be similar to how Instagram behaves when you load the grid of images. Something like below.

    p.load(imageurl).placeholder(R.drawable.localFile).fit().centerInside().error(R.drawable.ic_broken_image).into(view)

    1. Use Glide. Glide does have powerful capability to create Thumbnails. And you can load the Thumbnails into the view much faster than the full images.