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?
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.