The Problem
I have locally-generated data that I need to display in a RecyclerView
. I tried to use a custom PagingSource
with PagingDataAdapter
to reduce the amount of data in memory, but I get visual effects when I invalidate the data, for example if I insert or delete one item:
- when the visible items belong to 2 different "pages", many unchanged items flash as if they had been modified
- if all the visible items belong to the same page, everything is fine. Only the inserted / deleted item shows an animation.
I took an example application referenced by Google's doc (PagingSample) to test the concept. The original with Room does not show artefacts, but my modified version with custom PagingSource
does.
The code generated and used by Room is too complicated to see any difference that would explain the issue.
My data must be locally-generated, I can't use Room as a work-around to display them.
My question
How can I properly define a PagingSource
for my local data, and use it with PagingDataAdapter without visual glitches?
Optionally, how can I know when data is discarded (so I can discard my local data as well)?
Code excerpts and details
The full example project is hosted here: https://github.com/blueglyph/PagingSampleModified
Here is the data:
private val _data = ArrayMap<Int, Cheese>()
val data = MutableLiveData <Map<Int, Cheese>>(_data)
val sortedData = data.map { data -> data.values.sortedBy { it.name.lowercase() } }
and the PagingSource
. I'm using key = item position. I have tried with key = page number, each page containing 30 items (10 are visible), but it does not change anything.
private class CheeseDataSource(val dao: CheeseDaoLocal, val pageSize: Int): PagingSource<Int, Cheese>() {
fun max(a: Int, b: Int): Int = if (a > b) a else b
override fun getRefreshKey(state: PagingState<Int, Cheese>): Int? {
val lastPos = dao.count() - 1
val key = state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(pageSize)?.coerceAtMost(lastPos) ?: anchorPage?.nextKey?.minus(pageSize)?.coerceAtLeast(0)
}
return key
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Cheese> {
val pageNumber = params.key ?: 0
val count = dao.count()
val data = dao.allCheesesOrdName().drop(pageNumber).take(pageSize)
return LoadResult.Page(
data = data,
prevKey = if (pageNumber > 0) max(0, pageNumber - pageSize) else null,
nextKey = if (pageNumber + pageSize < count) pageNumber + pageSize else null
)
}
}
The Flow
on PagingData
is created in the view model:
val pageSize = 30
var dataSource: PagingSource<Int, Cheese>? = null
val allCheeses: Flow<PagingData<CheeseListItem>> = Pager(
config = PagingConfig(
pageSize = pageSize,
enablePlaceholders = false,
maxSize = 90
)
) {
dataSource = dao.getDataSource(pageSize)
dataSource!!
}.flow
.map { pagingData -> pagingData.map { cheese -> CheeseListItem.Item(cheese) } }
with dao.getDataSource(pageSize)
returning the CheeseDataSource
shown above.
and in the activity, data pages are collected and submitted:
lifecycleScope.launch {
viewModel.allCheeses.collectLatest { adapter.submitData(it) }
}
When the data is modified, an observer triggers an invalidation:
dao.sortedData.observeForever {
dataSource?.invalidate()
}
The scrolling and loading of pages is fine, the only problems come when invalidate
is used and when items from 2 pages are displayed simultaneously.
The adapter is classic:
class CheeseAdapter : PagingDataAdapter<CheeseListItem, CheeseViewHolder>(diffCallback) {
...
companion object {
val diffCallback = object : DiffUtil.ItemCallback<CheeseListItem>() {
override fun areItemsTheSame(oldItem: CheeseListItem, newItem: CheeseListItem): Boolean {
return if (oldItem is CheeseListItem.Item && newItem is CheeseListItem.Item) {
oldItem.cheese.id == newItem.cheese.id
} else if (oldItem is CheeseListItem.Separator && newItem is CheeseListItem.Separator) {
oldItem.name == newItem.name
} else {
oldItem == newItem
}
}
override fun areContentsTheSame(oldItem: CheeseListItem, newItem: CheeseListItem): Boolean {
return oldItem == newItem
}
}
...
What I have tried (among many other things)
- use LiveData instead of Flow
- using / removing the cache on the Flow
- removing the observers and invalidating directly in insert/delete functions to make the code more direct
- instead of key = position, using key = page number (0, 1, 2, ...) with each page containing pageSize=30 items
At this point, I'm not sure anymore that paging-3 is meant to be used for custom data. I'm observing so many operations for a simple insert/delete, such as 2000-4000 compare ops in the adapter, reloading 3 pages of data, ... that using ListAdapter
directly on my data and doing the load/unload manually seems a better option.