Search code examples
androidandroid-recyclerviewkotlinkotlinx.coroutines

How to save some data in a recycler adapter item to cancel requests in flight when a view is recyclerd?


I have a recycler adapter that loads content from the network as it is scrolled (including thumbnails and some other data).

However, when I scroll a really long distance, the adapter tries to queue up requests such that all of the items in between the start point and and point are loaded and then displayed before the new position's items are. I don't want to continue loading these intermediate items when they are scrolled past quickly like this.

I tried writing the below code to do this:

class SomeRecyclerAdapter(private val dataset: MutableList<SomeData>)
    : RecyclerView.Adapter<SongsRecylerAdapter.ViewHolder>() {

    // ... irrelevant fields here

    var requestInFlight: Job? = null
    var requestInFlight2: Job? = null

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val someListItem = LayoutInflater.from(parent.context)
                .inflate(R.layout.list_item_foo, parent, false)

        return ViewHolder(someListItem)
    }

    override fun getItemCount() = dataset.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val someData = dataset[position]
        holder.someView.text = someData.toString()
        holder.thumbnailView.imageResource = R.drawable.default_thumbnail

        // Cancel any requests currently in flight, to prevent janky stuff on long scrolls
        // Otherwise, if scrolling far, it will load all of the data in sequence
        // We don't want to load the stuff in between before loading the stuff where we are at
        requestInFlight?.cancel()
        requestInFlight2?.cancel()

        requestInFlight = async(UI) {
            var someDataFromServer = listOf<Bar>()

            async(CommonPool) {
                someDataFromServer = someSuspendMethod()
            }.await()

            someData.thumbnailUri = someDataFromServer.thumbnailUri

            GlideApp.with(holder.thumbnailView.context)
                    .load(dataset[position].thumbnailUri)
                    .placeholder(R.drawable.default_thumbnail)
                    .into(holder.thumbnailView)
        }
    }

    // Viewholder here...

}

So, I think I know why it's not working. The when I cancel the job in flight, I'm really cancelling the previous call that was launched, not the one for the recycler view element that is being reloaded. However, I'm not sure how to associate the job with this element, such that when this view gets recycled, it cancels it's own in-flight request.

Any help?


Solution

  • Make requestInFlight and requestInFlight2 into ViewHolder fields since each one has it's own loading, as it is now you're incorrectly overwriting the Jobs during scroll.

    Then move your cancellation logic into onViewRecycled:

    override fun onViewRecycled(holder: ViewHolder){
        holder.requestInFlight?.cancel()
        holder.requestInFlight2?.cancel()
    }
    

    Note that coroutines hold strong references to whatever object they reference inside async block, so You might leak context on screen rotation.