com.github.mvysny.vokdataloader.PageFetchingList.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of vok-dataloader Show documentation
Show all versions of vok-dataloader Show documentation
VOK-DataLoader: The Paged/Filtered/Sorted DataLoader API
The newest version!
package com.github.mvysny.vokdataloader
import java.lang.IndexOutOfBoundsException
/**
* Provides a random-access list interface on top of a [DataLoader]. This class keeps a cache of pages around most
* recently retrieved item; pages are loaded from the [loader] as necessary.
*
* At most three pages are cached. Not thread-safe. For example you can use this class to feed [DataLoader] to Android's `ListAdapter`.
*
* ## Volatility
*
* The `DataLoader` may load data from a volatile source: the number of items may change over time and there could be fewer
* of items available over time. Even if the callee would check for [size] regularly, there is no atomicity guarantee between
* a call to [size] and subsequent calls to [get]. Therefore, it would be possible to get [IndexOutOfBoundsException] exceptions
* from [get] even with utmost care.
*
* Therefore, the list offers you a possibility to use a special [dataMissingMarker] item that will be returned by [get] when there
* is not enough data. The callee should then handle such marker items, for example by providing a special "Refresh"/"More" button
* in place of the marker item, which allows the user to refresh the data.
*
* @param pageSize the size of the page. If this list will be used to display on-screen data, the best size of the page
* is the number of items visible at once in the scrolling view.
* @param dataMissingMarker if null and there is not enough data, [IndexOutOfBoundsException] is thrown. However, if not null,
* this marker item is returned, to inform the callee that the dataset has been changed and needs a refresh.
*/
public class PageFetchingList(
public val loader: DataLoader,
public val pageSize: Int,
public val dataMissingMarker: T? = null
): AbstractList() {
/**
* Remembers three pages closest to the last request for an item via [get]:
* the page containing the item, a successive page and a predecessor page.
*/
private val pages: MutableMap> = mutableMapOf>()
init {
require(pageSize >= 1) { "pageSize must be 1 or greater: $pageSize"}
}
override val size: Int
get() = loader.getCount(null).toInt()
override fun get(index: Int): T {
if (index < 0) {
throw IndexOutOfBoundsException("index must be 0 or greater: $index")
}
val pageIndex: Int = index / pageSize
pages.keys.retainAll(setOf(pageIndex - 1, pageIndex, pageIndex + 1))
if (pageIndex > 0) {
val prevPage: List = cachePage(pageIndex - 1)
if (prevPage.size < pageSize) {
if (dataMissingMarker != null) return dataMissingMarker
throw IndexOutOfBoundsException("Index $index resolved to page $pageIndex but fetching prev page yielded less than $pageSize items: ${prevPage.size}. Maybe the dataset has been changed. Reported total number of items: $size")
}
}
val page: List = cachePage(pageIndex)
if (page.size >= pageSize) {
cachePage(pageIndex + 1)
}
val pageOffset = index % pageSize
if (page.size <= pageOffset) {
if (dataMissingMarker != null) return dataMissingMarker
throw IndexOutOfBoundsException("Index $index resolved to page $pageIndex offset $pageOffset but that page contains less than $pageSize items: ${page.size}. Maybe the dataset has been changed. Reported total number of items: $size")
}
return page[pageOffset]
}
private fun cachePage(pageIndex: Int): List = pages.getOrPut(pageIndex) {
val startIndex: Int = pageIndex * pageSize
val range: LongRange = startIndex.toLong()..(startIndex + pageSize - 1).toLong()
val data: List = loader.fetch(range = range)
check(data.size <= pageSize) { "Asked for range $range of length ${range.length} (=$pageSize) but got ${data.size} items" }
data
}
override fun toString(): String =
"PageFetchingList(loader=$loader, pageSize=$pageSize, dataMissingMarker=$dataMissingMarker)"
/**
* A read-only view of the current cache. Maps page index to the list of items retrieved from the [loader].
*/
public val cache: Map> get() = pages
}
/**
* Returns a [List] which lazily fetches data from the underlying data loader. See [PageFetchingList] for more details.
*/
public fun DataLoader.asList(pageSize: Int): List = PageFetchingList(this, pageSize)
© 2015 - 2025 Weber Informatics LLC | Privacy Policy