All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.mvysny.vokdataloader.PageFetchingList.kt Maven / Gradle / Ivy

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