{"id":20182795,"url":"https://github.com/jamal-wia/paginator","last_synced_at":"2026-05-30T10:01:09.345Z","repository":{"id":218059899,"uuid":"745495585","full_name":"jamal-wia/Paginator","owner":"jamal-wia","description":"Kotlin Multiplatform pagination library for Android, iOS, JVM \u0026 Web — cursor \u0026 offset, caching, bookmarks, Flow-based state","archived":false,"fork":false,"pushed_at":"2026-05-24T17:38:20.000Z","size":28222,"stargazers_count":98,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-05-24T18:05:27.476Z","etag":null,"topics":["android","clean-architecture","coroutines","cursor-pagination","flow","infinite-scroll","ios","jetpack-paging-alternative","kmp","kotlin","kotlin-multiplatform","pagination","paginator","paging"],"latest_commit_sha":null,"homepage":"https://deepwiki.com/jamal-wia/Paginator","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jamal-wia.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-01-19T13:12:24.000Z","updated_at":"2026-05-24T17:34:32.000Z","dependencies_parsed_at":"2024-02-21T23:22:50.186Z","dependency_job_id":"fbf4c1eb-6642-40f4-b869-5d4866d61ae8","html_url":"https://github.com/jamal-wia/Paginator","commit_stats":null,"previous_names":["jamal-wia/paginator"],"tags_count":75,"template":false,"template_full_name":null,"purl":"pkg:github/jamal-wia/Paginator","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jamal-wia%2FPaginator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jamal-wia%2FPaginator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jamal-wia%2FPaginator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jamal-wia%2FPaginator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jamal-wia","download_url":"https://codeload.github.com/jamal-wia/Paginator/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jamal-wia%2FPaginator/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33687722,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-30T02:00:06.278Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","clean-architecture","coroutines","cursor-pagination","flow","infinite-scroll","ios","jetpack-paging-alternative","kmp","kotlin","kotlin-multiplatform","pagination","paginator","paging"],"created_at":"2024-11-14T02:42:25.286Z","updated_at":"2026-05-30T10:01:09.338Z","avatar_url":"https://github.com/jamal-wia.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Paginator — KMP pagination library for Android, iOS, JVM \u0026 Web\n\n\u003e Pagination / paging library for **Kotlin Multiplatform** (Android, iOS, JVM, Desktop, Server, JS,\n\u003e Wasm).\n\u003e A pure-Kotlin alternative to **Jetpack Paging 3** with cursor pagination, bidirectional scroll,\n\u003e bookmarks, page caching, element-level CRUD, infinite scroll, prefetch and Flow-based UI state.\n\n**Keywords:** Kotlin Multiplatform pagination · KMP paging · Android paging library · iOS paging ·\nKotlin JS pagination · Kotlin Wasm pagination · web pagination · cursor pagination · infinite\nscroll ·\nendless list · load more · Jetpack Paging 3 alternative · bidirectional pagination ·\nchat / messenger feed · GraphQL connections · coroutines · Flow.\n\n[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/jamal-wia/Paginator)\n[![Maven Central](https://img.shields.io/maven-central/v/io.github.jamal-wia/paginator)](https://central.sonatype.com/artifact/io.github.jamal-wia/paginator) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)\n![Kotlin Multiplatform](https://img.shields.io/badge/Kotlin-Multiplatform-7F52FF?logo=kotlin)\n![Android](https://img.shields.io/badge/target-Android-green)\n![JVM](https://img.shields.io/badge/target-JVM-blue)\n![iOS](https://img.shields.io/badge/target-iOS-lightgrey)\n![JS](https://img.shields.io/badge/target-JS-f7df1e?logo=javascript\u0026logoColor=black)\n![WasmJs](https://img.shields.io/badge/target-WasmJs-654ff0?logo=webassembly\u0026logoColor=white)\n\n## [**📲 Download Demo APK**](https://raw.githubusercontent.com/jamal-wia/Paginator/master/PaginatorDemo.apk)\n\n**Paginator** is a powerful, flexible **pagination library for Kotlin Multiplatform (KMP)** —\nAndroid, iOS, JVM, Desktop, JS and Wasm — that goes far beyond simple \"load next page\" patterns. It\nis a\nproduction-ready alternative to **Jetpack Paging 3** / `Pager` / `AsyncPagingDataDiffer` with a\nfull-featured page management system: jumping to arbitrary pages, bidirectional navigation,\nbookmarks, page caching, cursor-based pagination, element-level CRUD, incomplete page handling,\ncapacity management and reactive UI state via Kotlin Flows.\n\nThe library is split into a **shared core** plus **two pagination strategies** that you import\nindependently, so a consumer only pays for the variant they actually need:\n\n- **`paginator-offset`** — `Paginator` / `MutablePaginator`, offset/page-number addressing\n  (`MutableList`-like).\n- **`paginator-cursor`** — `CursorPaginator` / `MutableCursorPaginator`, cursor-based\n  `prev`/`self`/`next` linked navigation (`LinkedList`-like). See\n  [Cursor-Based Pagination](docs/13.%20cursor-pagination.md).\n\nBoth build on `paginator-core`, which holds the shared page-state model, caches, CRUD surface, UI\nstate and snapshot flow. UI bindings ship as matching pairs:\n`paginator-compose-offset` / `paginator-compose-cursor` for Compose Multiplatform and\n`paginator-view-offset` / `paginator-view-cursor` for Android `RecyclerView`.\n\nBuilt entirely with pure Kotlin and without platform-specific dependencies, \nPaginator can be seamlessly used across all layers of an application \n— from data to domain to presentation — while preserving Clean Architecture principles and proper layer separation.\n\n**Supported targets:** Android · JVM · iosX64 · iosArm64 · iosSimulatorArm64 · js · wasmJs\n\n---\n## AI Docs - https://deepwiki.com/jamal-wia/Paginator\n---\n\n## Table of Contents\n\n- [Why Paginator? (vs Jetpack Paging 3)](#why-paginator-vs-jetpack-paging-3)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Infinite Scroll / Infinite Feed](#infinite-scroll--infinite-feed)\n- [Cursor-Based Pagination](#cursor-based-pagination)\n- [Features](#features)\n- [Articles](#articles)\n- [Documentation](#documentation)\n- [License](#license)\n\n---\n\n## Why Paginator? (vs Jetpack Paging 3)\n\nMost Android developers reach for **Jetpack Paging 3**, which is Android-centric in practice\n(KMP targets exist in upstream sources, but the published artifacts and ecosystem — Room,\nRecyclerView, Compose adapters — are Android-first), ViewModel/UI-coupled and intentionally\nopinionated. Paginator was built for the cases Paging 3 doesn't cover well:\n\n| Capability                              | Jetpack Paging 3                                   | **Paginator**                                                    |\n|-----------------------------------------|----------------------------------------------------|------------------------------------------------------------------|\n| Targets (published artifacts)           | Android-first (KMP in sources, ecosystem AndroidX) | **Android · iOS · JVM · Desktop · JS · WasmJs**                  |\n| Layer                                   | UI / ViewModel                                     | **Data / Domain / Presentation**                                 |\n| Bidirectional scroll (chat / messenger) | Limited                                            | **First-class**                                                  |\n| Cursor pagination (GraphQL, chat)       | Manual                                             | **Built-in `CursorPaginator`**                                   |\n| Jump to arbitrary page / bookmarks      | No                                                 | **Yes (`jump`, `BookmarkInt`, …)**                               |\n| Element-level CRUD inside pages         | No (immutable `PagingData`)                        | **Yes (`MutablePaginator`)**                                     |\n| Page caching strategies                 | Fixed window around viewport (`maxSize`)           | **Pluggable: MostRecent / Queued / TimeLimited / ContextWindow** |\n| State serialization (process death)     | Last `PagingData` only via `cachedIn`              | **Full cache via `kotlinx.serialization`**                       |\n| Dependencies                            | AndroidX                                           | **Pure Kotlin, zero platform deps**                              |\n\nSee the side-by-side\nwrite-up: [Paging 3 is good. Until you need something more.](articles/en/Paging%203%20is%20good.%20Until%20you%20need%20something%20more.md)\n\n---\n\n## Installation\n\nThe library is published to **Maven Central**. No additional repository configuration needed.\n\nSince **10.0.0** the suite is split into per-strategy modules so consumers only pay for the variant\nthey actually use:\n\n| Artifact                                       | When to pick it                                                          |\n|------------------------------------------------|--------------------------------------------------------------------------|\n| `io.github.jamal-wia:paginator-core`           | Pulled transitively by everything below. Never declared directly.        |\n| `io.github.jamal-wia:paginator-offset`         | Page-number (`MutableList`-like) `Paginator` / `MutablePaginator`        |\n| `io.github.jamal-wia:paginator-cursor`         | Cursor / GraphQL-connection `CursorPaginator` / `MutableCursorPaginator` |\n| `io.github.jamal-wia:paginator-compose-offset` | Compose Multiplatform bindings for the offset paginator                  |\n| `io.github.jamal-wia:paginator-compose-cursor` | Compose Multiplatform bindings for the cursor paginator                  |\n| `io.github.jamal-wia:paginator-view-offset`    | Android `RecyclerView` bindings for the offset paginator                 |\n| `io.github.jamal-wia:paginator-view-cursor`    | Android `RecyclerView` bindings for the cursor paginator                 |\n| `io.github.jamal-wia:paginator-bom`            | Bill of Materials — pins every paginator-* artifact to the same version  |\n\nThe recommended way is to import the **BOM** once and then declare only the artifacts you actually\nuse — the BOM keeps the suite aligned on your classpath:\n\n```kotlin\ndependencies {\n  // Pin all Paginator artifacts together. Latest version: see the Maven Central badge above.\n  implementation(platform(\"io.github.jamal-wia:paginator-bom:10.0.0\"))\n\n  // Pick exactly one paginator strategy (or both, if your app needs both)\n  implementation(\"io.github.jamal-wia:paginator-offset\")   // page-number paginator\n  // implementation(\"io.github.jamal-wia:paginator-cursor\")  // cursor / GraphQL-connection paginator\n\n  // Optional UI bindings — match the strategy you picked above\n  implementation(\"io.github.jamal-wia:paginator-compose-offset\")  // Compose Multiplatform — offset\n  implementation(\"io.github.jamal-wia:paginator-view-offset\")     // Android RecyclerView — offset\n  // implementation(\"io.github.jamal-wia:paginator-compose-cursor\")\n  // implementation(\"io.github.jamal-wia:paginator-view-cursor\")\n}\n```\n\n`paginator-core` is brought in automatically by every other module — never declare it directly.\n\nFor **Kotlin Multiplatform**, Gradle automatically resolves the correct platform artifact\n(`paginator-offset-jvm`, `paginator-offset-iosArm64`, `paginator-offset-js`, etc.) from the KMP\nmetadata. The `paginator-compose-*` modules go in the shared Compose source set;\n`paginator-view-*` is Android-only and belongs in the Android source set.\n\n\u003e **Kotlin 2.3+ / KMP note:** calling `platform()` inside\n`kotlin { sourceSets { commonMain.dependencies { } } }`\n\u003e is deprecated ([KT-58759](https://youtrack.jetbrains.com/issue/KT-58759)) and scheduled for\n\u003e removal.\n\u003e Declare the BOM in the **top-level** `dependencies {}` block using `commonMainImplementation`:\n\u003e\n\u003e ```kotlin\n\u003e // top-level dependencies {} block — NOT inside kotlin { sourceSets { } }\n\u003e dependencies {\n\u003e     commonMainImplementation(platform(\"io.github.jamal-wia:paginator-bom:10.0.0\"))\n\u003e }\n\u003e\n\u003e // inside kotlin { sourceSets { commonMain.dependencies { } } } — no version needed\n\u003e kotlin {\n\u003e     sourceSets {\n\u003e         commonMain.dependencies {\n\u003e             implementation(\"io.github.jamal-wia:paginator-offset\")\n\u003e             implementation(\"io.github.jamal-wia:paginator-compose-offset\")\n\u003e         }\n\u003e     }\n\u003e }\n\u003e ```\n\nThe BOM only pins *Paginator* artifacts; it does not constrain Compose, Kotlin, AndroidX,\nor anything else on your classpath.\n\n**Supported targets**\n\n- `paginator-core`, `paginator-offset`, `paginator-cursor`:\n  Android · JVM · iosX64 · iosArm64 · iosSimulatorArm64 · js · wasmJs.\n- `paginator-compose-offset`, `paginator-compose-cursor`:\n  Android · JVM · iosArm64 · iosSimulatorArm64 · js · wasmJs.\n  (No iosX64 — Compose Multiplatform itself dropped that variant, as Apple deprecated the\n  Intel-Mac simulator. Apple-Silicon Macs use `iosSimulatorArm64`.)\n- `paginator-view-offset`, `paginator-view-cursor`: Android only.\n\n### Migrating from 8.x\n\nThe 8.x line shipped a single `paginator` artifact (+ `paginator-compose`, `paginator-view`).\n10.0.0 replaces them with the table above and renames Kotlin packages accordingly. Find/replace\nin your project:\n\n| 8.x import                                                                                    | 10.0.0 import                                                         |\n|-----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------|\n| `com.jamal_aliev.paginator.Paginator`                                                         | `com.jamal_aliev.paginator.offset.Paginator`                          |\n| `com.jamal_aliev.paginator.MutablePaginator`                                                  | `com.jamal_aliev.paginator.offset.MutablePaginator`                   |\n| `com.jamal_aliev.paginator.PagingCore`                                                        | `com.jamal_aliev.paginator.offset.PagingCore`                         |\n| `com.jamal_aliev.paginator.CursorPaginator`                                                   | `com.jamal_aliev.paginator.cursor.CursorPaginator`                    |\n| `com.jamal_aliev.paginator.bookmark.BookmarkInt`                                              | `com.jamal_aliev.paginator.offset.bookmark.BookmarkInt`               |\n| `com.jamal_aliev.paginator.bookmark.CursorBookmark`                                           | `com.jamal_aliev.paginator.cursor.bookmark.CursorBookmark`            |\n| `com.jamal_aliev.paginator.bookmark.Bookmark`                                                 | `com.jamal_aliev.paginator.core.bookmark.Bookmark`                    |\n| `com.jamal_aliev.paginator.load.LoadResult`                                                   | `com.jamal_aliev.paginator.offset.load.LoadResult`                    |\n| `com.jamal_aliev.paginator.load.CursorLoadResult`                                             | `com.jamal_aliev.paginator.cursor.load.CursorLoadResult`              |\n| `com.jamal_aliev.paginator.page.*`                                                            | `com.jamal_aliev.paginator.core.page.*`                               |\n| `com.jamal_aliev.paginator.exception.LockedException`                                         | `com.jamal_aliev.paginator.core.exception.LockedException`            |\n| `com.jamal_aliev.paginator.logger.*`                                                          | `com.jamal_aliev.paginator.core.logger.*`                             |\n| `com.jamal_aliev.paginator.cache.PagingCache`                                                 | `com.jamal_aliev.paginator.core.cache.PagingCache`                    |\n| `com.jamal_aliev.paginator.cache.InMemoryPagingCache`                                         | `com.jamal_aliev.paginator.offset.cache.InMemoryPagingCache`          |\n| `com.jamal_aliev.paginator.cache.eviction.CacheEvictionListener`                              | `com.jamal_aliev.paginator.core.cache.eviction.CacheEvictionListener` |\n| `com.jamal_aliev.paginator.cache.eviction.{Most,Queued,TimeLimited,ContextWindow}PagingCache` | `com.jamal_aliev.paginator.offset.cache.eviction.…`                   |\n| `com.jamal_aliev.paginator.compose.*`                                                         | `com.jamal_aliev.paginator.compose.offset.*` (or `.cursor.*`)         |\n| `com.jamal_aliev.paginator.view.*`                                                            | `com.jamal_aliev.paginator.view.offset.*` (or `.cursor.*`)            |\n\nRule of thumb: types whose name starts with `Cursor*` live in `com.jamal_aliev.paginator.cursor`,\n`Paginator` / `MutablePaginator` / `BookmarkInt` / `LoadResult` and the page-number cache /\neviction / serialization helpers live in `com.jamal_aliev.paginator.offset`, everything else\n(bookmarks base, page state, logger, exceptions, prefetch options, reactive cache plumbing) lives\nin `com.jamal_aliev.paginator.core`.\n\n### What each UI artifact does\n\n`paginator-compose-offset` / `paginator-compose-cursor` provide scroll-driven prefetch for\n`LazyColumn` / `LazyRow` / `LazyVerticalGrid` / `LazyVerticalStaggeredGrid` (and horizontal\ncounterparts) — no manual `LaunchedEffect` / `snapshotFlow` plumbing. The recommended entry point\nis `rememberPaginated` + the `paginated { }` DSL — zero manual numbers (`dataItemCount` is read\nfrom `paginator.uiState`, header / footer counts are tallied by the DSL):\n\n```kotlin\nval listState = rememberLazyListState()\nval paged = paginator.rememberPaginated(state = listState)\n\nLazyColumn(state = listState) {\n    paginated(paged) {\n        header { StickyTitle() }\n        items(uiState.items, key = { it.id }) { Row(it) }\n        appendIndicator { AppendIndicator(uiState.appendState) }\n    }\n}\n```\n\nA one-call `PrefetchOnScroll(state, dataItemCount, …)` and a low-level\n`rememberPrefetchController` + `BindToLazyList` are also available if you want to keep\ncounts explicit or hold a reference to the controller. See\n[docs/7. prefetch.md](docs/7.%20prefetch.md) for the full\nguide — including `PrefetchOptions`, reactive error handling via `PrefetchErrorChannel`,\nand advanced knobs (`restartKey`, `scrollSampleMillis`).\n\n`paginator-view-offset` / `paginator-view-cursor` remove the `OnScrollListener` plumbing entirely\nand offer three layers of integration: `bindPaginated` (auto-tracks `dataItemCount` from\n`paginator.uiState`), `bindPrefetchToRecyclerView` (one-call factory + bind), and the\nlow-level `controller.bindToRecyclerView` for `ViewModel`-scoped controllers. All three\nsupport `LinearLayoutManager`, `GridLayoutManager`, and `StaggeredGridLayoutManager`,\ninstall both `OnScrollListener` and `OnLayoutChangeListener` (so partial first pages\ndon't stall pagination), and clean up on `ON_DESTROY`.\n\n```kotlin\nbinding.recyclerView.adapter = ConcatAdapter(headerAdapter, dataAdapter, appendIndicatorAdapter)\nbinding.recyclerView.layoutManager = LinearLayoutManager(context)\n\nval paged = paginator.bindPaginated(\n    recyclerView = binding.recyclerView,\n    lifecycleOwner = viewLifecycleOwner,\n    headerCount = { headerAdapter.itemCount },\n    footerCount = { appendIndicatorAdapter.itemCount },\n)\n// `paged.controller` for hot-update mutation; `paged.recalibrate()` after refresh().\n```\n\nReactive prefetch errors via `PrefetchErrorChannel` (StateFlow), runtime knobs through\n`PrefetchOptions` (shared with the Compose binding), and stable `PageLoadGuard` /\n`CursorLoadGuard` are also available.\n\nSee [docs/7. prefetch.md](docs/7.%20prefetch.md) for details.\n\n---\n\n## Quick Start\n\n### Step 1: Create a Paginator\n\nThe simplest way to create a `MutablePaginator` is via the DSL builder:\n\n```kotlin\nimport com.jamal_aliev.paginator.offset.dsl.mutablePaginator\nimport com.jamal_aliev.paginator.offset.load.LoadResult\n\nclass FeedViewModel : ViewModel() {\n\n    private val paginator = mutablePaginator\u003cItem\u003e {\n        load { page -\u003e LoadResult(repository.loadPage(page)) }\n    }\n}\n```\n\nThe `load { }` block is the only required call — every other knob (capacity, cache strategy,\nlogger, bookmarks, custom `PageState` factories) has sensible defaults. See\n[DSL Builder](docs/11.%20dsl-builder.md) for the full configuration surface.\n\nIf you only need read-only navigation, use `paginator\u003cT\u003e { … }` instead — it returns a\n`Paginator\u003cT\u003e`, so element-level mutations are not exposed at the call site.\n\nThe `load` lambda receives an `Int` page number and should return a `LoadResult\u003cT\u003e` wrapping\nyour data list. For the simplest case, just wrap with `LoadResult(list)`. The direct constructor\nform (`MutablePaginator(load = { … })`) is also still available if you prefer it.\n\n### Step 2: Observe and Start\n\nSubscribe to `paginator.uiState` to receive UI updates, then start the paginator by jumping to the\nfirst page:\n\n```kotlin\ninit {\n  paginator.uiState\n    .onEach { state -\u003e\n      when (state) {\n        is PaginatorUiState.Content -\u003e showContent(state.items)\n        is PaginatorUiState.Loading -\u003e showLoading()\n        is PaginatorUiState.Empty -\u003e showEmpty()\n        is PaginatorUiState.Error -\u003e showError(state.cause)\n        is PaginatorUiState.Idle -\u003e Unit\n      }\n    }\n        .flowOn(Dispatchers.Main)\n        .launchIn(viewModelScope)\n\n    viewModelScope.launch {\n        paginator.jump(bookmark = BookmarkInt(page = 1))\n    }\n}\n```\n\n`uiState` emits `Idle` / `Loading` / `Empty` / `Error` / `Content(items, prependState, appendState)`\nso your UI does not have to reason about individual `PageState`s. If you need raw page-level access,\ncollect `paginator.core.snapshot` instead. See\n[State, Transactions \u0026 Locks → PaginatorUiState](docs/3.%20state.md#paginatoruistate).\n\n### Step 3: Navigate\n\n```kotlin\n// Load next page (triggered by scroll reaching the end)\nfun loadMore() {\n    viewModelScope.launch { paginator.goNextPage() }\n}\n\n// Load previous page (triggered by scroll reaching the top)\nfun loadPrevious() {\n    viewModelScope.launch { paginator.goPreviousPage() }\n}\n```\n\n### Step 4: Release\n\nWhen the paginator is no longer needed, release its resources:\n\n```kotlin\noverride fun onCleared() {\n    paginator.release()\n    super.onCleared()\n}\n```\n\n---\n\n## Infinite Scroll / Infinite Feed\n\nPaginator works perfectly for a simple infinite scroll — and this is a first-class use case, not an\nafterthought.\n\nEvery feature in the library is **strictly opt-in**. If all you need is \"load the next page when the\nuser scrolls down\", the entire setup is what you already saw in Quick Start: one `load` lambda,\none `uiState` observer, and `goNextPage()` on scroll. Nothing else is required.\n\nWhat you still get for free, with zero extra code:\n\n- `ProgressPage` while the next page loads — no manual loading flag needed\n- `ErrorPage` with the previously cached data intact — a failed request won't clear the screen\n- Incomplete page detection — if the server returns fewer items than expected, the paginator quietly\n  re-requests on the next scroll instead of silently stopping\n\nStart with the simplest setup. Adopt advanced features only if and when your product actually needs\nthem.\n\n---\n\n## Cursor-Based Pagination\n\nIf your backend returns opaque continuation tokens instead of numeric page offsets (GraphQL\nconnections, chat feeds, activity streams, Slack/Instagram/Reddit-style APIs), reach for the\ncursor variant:\n\n```kotlin\nimport com.jamal_aliev.paginator.cursor.bookmark.CursorBookmark\nimport com.jamal_aliev.paginator.cursor.dsl.mutableCursorPaginator\nimport com.jamal_aliev.paginator.cursor.load.CursorLoadResult\n\nval messages = mutableCursorPaginator\u003cMessage\u003e(capacity = 50) {\n  load { cursor -\u003e\n    val page = api.getMessages(cursor?.self as? String)\n    CursorLoadResult(\n      data = page.items,\n      bookmark = CursorBookmark(\n        prev = page.prevCursor,   // null at the head of the feed\n        self = page.selfCursor,   // required — cache key\n        next = page.nextCursor,   // null at the tail of the feed\n      ),\n    )\n  }\n}\n\nviewModelScope.launch {\n  messages.restart()           // bootstrap from the first cursor (or initialCursor if set)\n  messages.goNextPage()        // follows endContextCursor.next — throws EndOfCursorFeedException at tail\n  messages.goPreviousPage()    // follows startContextCursor.prev — throws at head\n}\n```\n\nThe cursor paginator shares caches, CRUD, UI state (`paginator.uiState`), snapshot flow,\n`transaction { }`, prefetch controller, logger, and serialization with the offset variant — it\ndiffers only in **how pages are addressed**. Read the full guide at\n[Cursor-Based Pagination](docs/13.%20cursor-pagination.md).\n\n---\n\n## Features\n\n- **Two pagination flavours** -- offset-based `Paginator` (`MutableList`-like, numeric page\n  addressing) and cursor-based `CursorPaginator` (`LinkedList`-like, `prev`/`self`/`next` tokens)\n  sharing the same page-state model, caches, CRUD surface, UI state and snapshot flow. See\n  [Cursor-Based Pagination](docs/13.%20cursor-pagination.md)\n- **Bidirectional pagination** -- navigate forward (`goNextPage`) and backward (`goPreviousPage`)\n- **Jump to any page** -- jump to arbitrary pages with `jump(bookmark)`\n- **Bookmark system** -- define bookmarks and cycle through them with `jumpForward` / `jumpBack`,\n  with optional recycling (wrap-around)\n- **Incomplete page handling** -- when the server returns fewer items than expected, the paginator\n  detects this and re-requests the page on the next `goNextPage`, showing cached data with a loading\n  indicator\n- **Final page limit** -- set `finalPage` to enforce a maximum page boundary (typically from backend\n  metadata), throwing `FinalPageExceededException` when exceeded\n- **Page caching** -- loaded pages are cached in a sorted map for instant access\n- **Cache eviction strategies** -- pluggable eviction via decorator subclasses of `PagingCore`:\n  `MostRecentPagingCache` (LRU), `QueuedPagingCache` (FIFO), `TimeLimitedPagingCache` (TTL),\n  and `ContextWindowPagingCache` (evicts outside context). Eviction listener callback for reacting\n  to\n  page removal\n- **Reactive state** -- observe page changes via `snapshot` Flow (visible pages) or `asFlow()` (\n  entire cache)\n- **High-level UI state** -- `paginator.uiState: Flow\u003cPaginatorUiState\u003cT\u003e\u003e` collapses the raw\n  snapshot into `Idle` / `Loading` / `Empty` / `Error` / `Content(items, prependState, appendState)`\n  for screens that only need full-screen indicators and boundary activity markers\n- **Element-level CRUD** -- get, set, add, remove, and replace individual elements within pages,\n  with automatic page rebalancing\n- **Capacity management** -- resize pages on the fly with automatic data redistribution\n- **Source metadata** -- `load` returns `LoadResult\u003cT\u003e`, an open wrapper that carries both\n  page data and arbitrary metadata from the API response (total count, cursors, etc.). Metadata\n  flows through initializer lambdas into custom `PageState` subclasses\n- **Custom PageState subclasses** -- extend `SuccessPage`, `ErrorPage`, `ProgressPage`, or\n  `EmptyPage` with your own types via initializer lambdas\n- **Dirty pages** -- mark pages as \"dirty\" so they are automatically refreshed (fire-and-forget) on\n  the next navigation (`goNextPage`, `goPreviousPage`, `jump`). CRUD operations can also mark pages\n  dirty via the `isDirty` flag\n- **Two-tier API** -- `Paginator` (read-only navigation, dirty tracking, release) and\n  `MutablePaginator` (element-level CRUD, resize, public `setState`)\n- **DSL builder** -- declarative `paginator\u003cT\u003e { … }` and `mutablePaginator\u003cT\u003e { … }` blocks that\n  collapse `PagingCore` setup, cache composition, bookmarks, logger and custom `PageState`\n  initializers into one configuration site\n- **Rich extension API** -- collection-style helpers on `Paginator` (`find`, `count`, `flatten`,\n  `firstOrNull`, `contains`, …) and bulk CRUD on `MutablePaginator` (`prependElement`,\n  `moveElement`, `swapElements`, `insertBefore`/`After`, `removeAll`, `retainAll`, `distinctBy`,\n  `updateAll`/`updateWhere`)\n- **Lock flags** -- prevent specific operations at runtime (`lockJump`, `lockGoNextPage`,\n  `lockGoPreviousPage`, `lockRestart`, `lockRefresh`)\n- **Scroll-based prefetch** -- `PaginatorPrefetchController` monitors scroll position and\n  automatically loads the next/previous page before the user reaches the edge of content,\n  configurable at runtime via `PrefetchOptions` (prefetch distance, backward prefetch,\n  scroll sampling, cancel-on-dispose, …) shared across all UI artifacts\n- **Reactive prefetch errors** -- `PrefetchErrorChannel` exposes prefetch failures as a\n  `StateFlow`, so silent background loads can still surface to the UI without coupling to\n  the controller\n- **Stable load guards** -- `PageLoadGuard` / `CursorLoadGuard` functional interfaces gate\n  prefetch on custom conditions (network state, quotas, rate limits) without subclassing\n- **Index remapping** -- `RemapIndices` translates UI scroll positions (with headers,\n  footers, dividers) into data-only coordinates, so prefetch math stays correct in mixed\n  `ConcatAdapter` / DSL layouts\n- **Parallel loading** -- preload multiple pages concurrently with `loadOrGetPageState`\n- **Pluggable logging** -- implement the `PaginatorLogger` interface to receive detailed logs about\n  navigation, state changes, and element-level operations. No logging by default (`null`)\n- **State serialization** -- save and restore the paginator's cache to/from JSON via\n  `kotlinx.serialization`, enabling seamless recovery after process death on any KMP target\n- **Transaction** -- execute a block of operations atomically with `transaction { }`. If any\n  exception occurs (including coroutine cancellation), the entire paginator state is rolled back\n- **Context window** -- the paginator tracks a contiguous range of successfully loaded pages (\n  `startContextPage..endContextPage`), which defines the visible snapshot\n- **Interweaving** -- opt-in `Flow\u003cPaginatorUiState\u003cT\u003e\u003e.interweave(weaver)` operator that inserts\n  meta-rows (date headers, unread dividers, section labels, …) between data items without touching\n  the paginator core, cache, CRUD, serialization, or DSL\n- **Per-strategy modules** -- pick `paginator-offset` for page-number feeds and / or\n  `paginator-cursor` for cursor / GraphQL-connection feeds; both build on `paginator-core` and\n  ship matching UI bindings, so consumers never pull the strategy they don't use\n- **Bill of Materials (`paginator-bom`)** -- import the BOM once and declare any of the\n  paginator-* artifacts without versions; the BOM keeps the suite aligned on your classpath\n  and only constrains Paginator artifacts (no impact on Compose / Kotlin / AndroidX versions)\n- **Compose Multiplatform bindings (`paginator-compose-offset` / `paginator-compose-cursor`)** --\n  `PaginatedLazyList`, `PaginatedLazyGrid`, `PaginatedLazyStaggeredGrid` plus `rememberPaginated`\n  + the `paginated { }` DSL for zero-boilerplate prefetch on `LazyColumn` / `LazyRow` /\n  `LazyVerticalGrid` / `LazyVerticalStaggeredGrid` (and horizontal counterparts); a\n  one-call `PrefetchOnScroll(state, dataItemCount, …)` and a low-level\n  `rememberPrefetchController` + `BindToLazyList` / `BindToLazyGrid` /\n  `BindToLazyStaggeredGrid` are also available for explicit-count or controller-scoped\n  setups\n- **Android RecyclerView bindings (`paginator-view-offset` / `paginator-view-cursor`)** --\n  three layers of integration: `bindPaginated` (auto-tracks `dataItemCount` from\n  `paginator.uiState`), `bindPrefetchToRecyclerView` (one-call factory + bind), and a low-level\n  `controller.bindToRecyclerView` for `ViewModel`-scoped controllers; works with\n  `LinearLayoutManager`, `GridLayoutManager`, `StaggeredGridLayoutManager`, installs both\n  `OnScrollListener` and `OnLayoutChangeListener` (so partial first pages don't stall),\n  and cleans up on `ON_DESTROY`\n\n---\n\n## Articles\n\nIn-depth articles comparing Paginator with Jetpack Paging 3 and demonstrating real-world\nimplementation patterns:\n\n**English**\n\n- [Paging 3 is good. Until you need something more.](articles/en/Paging%203%20is%20good.%20Until%20you%20need%20something%20more.md) —\n  side-by-side comparison of Paginator and Paging 3 on a realistic feed\n- [Messenger on Paginator. Real-world tasks.](articles/en/Messenger%20on%20Paginator.%20Real-world%20tasks.md) —\n  building a production-grade messenger feed: bidirectional scroll, cursor pagination, CRUD,\n  interweaving\n- [Why I wrote Paginator instead of Paging 3.](articles/en/Why%20I%20wrote%20Paginator%20instead%20of%20Paging%203.md) —\n  the author's perspective: which design decisions in Paging 3 hit a ceiling and how Paginator\n  is built to avoid them\n\n**Русский**\n\n- [Paging 3 хорош. Пока вам не понадобится что-то ещё.](articles/ru/Paging%203%20хорош.%20Пока%20вам%20не%20понадобится%20что-то%20ещё.md) —\n  сравнение Paginator и Paging 3 на реальном примере ленты\n- [Мессенджер на Paginator. Боевые задачи](articles/ru/Мессенджер%20на%20Paginator.%20Боевые%20задачи.md) —\n  реализация мессенджера: двунаправленный скролл, курсорная пагинация, CRUD, interweaving\n- [Почему я написал Paginator вместо Paging 3.](articles/ru/Почему%20я%20написал%20Paginator%20вместо%20Paging%203.md) —\n  взгляд автора: где решения Paging 3 упираются в потолок и как Paginator устроен, чтобы это\n  обойти\n- [Как мигрировать с Paging 3 на Paginator.](articles/ru/Как%20мигрировать%20с%20Paging%203%20на%20Paginator.md) —\n  пошаговое руководство по переезду: карта соответствий API, замена `PagingSource` / `Pager` /\n  `PagingDataAdapter` / `LazyPagingItems` / `RemoteMediator`, чек-лист и типичные грабли\n\n---\n\n## Documentation\n\nDetailed documentation lives in the [`docs/`](docs/) directory:\n\n1. [**Core Concepts**](docs/1.%20core-concepts.md) — `PageState`, `Paginator` vs `MutablePaginator`,\n  context window, bookmarks, `LoadResult` \u0026 metadata, capacity, final page limit\n2. [**Navigation**](docs/2.%20navigation.md) — `goNextPage`, `goPreviousPage`, `jump`,\n   `jumpForward` /\n  `jumpBack`, `restart`, `refresh`\n3. [**State, Transactions \u0026 Locks**](docs/3.%20state.md) — dirty pages, reactive state (snapshot \u0026\n  cache flows), atomic `transaction { }`, lock flags\n4. [**Element Operations \u0026 Custom Page States**](docs/4.%20elements.md) — element-level CRUD, custom\n  `PageState` subclasses, `PlaceholderPageState`, metadata propagation\n5. [**State Serialization**](docs/5.%20serialization.md) — saving \u0026 restoring paginator state via\n  `kotlinx.serialization`, surviving process death\n6. [**Caching**](docs/6.%20caching.md) — eviction strategies (MostRecent, Queued, TimeLimited,\n   ContextWindow),\n   composing\n   strategies, persistent L2 cache\n7. [**Prefetch**](docs/7.%20prefetch.md) — auto-pagination on scroll with\n   `PaginatorPrefetchController`\n8. [**Logger**](docs/8.%20logger.md) — pluggable logging via `PaginatorLogger`\n9. [**Extensions**](docs/9.%20extensions.md) — extension function reference (`PageExt`,\n   iteration, search/aggregation, CRUD, refresh, prefetch) plus a complete ViewModel example\n10. [**API Reference**](docs/10.%20api-reference.md) — complete property / method / operator tables\n11. [**DSL Builder**](docs/11.%20dsl-builder.md) — `paginator\u003cT\u003e { … }` and\n    `mutablePaginator\u003cT\u003e { … }` builder DSL\n12. [**Interweaving**](docs/12.%20interweaving.md) — opt-in `Flow` operator that interleaves\n    meta-rows (date headers, unread dividers, …) between data items\n13. [**Cursor-Based Pagination**](docs/13.%20cursor-pagination.md) — `CursorPaginator` /\n    `MutableCursorPaginator` for opaque-token feeds (GraphQL connections, chat, activity streams)\n14. [**Paginator vs CursorPaginator**](docs/14.%20paginator-vs-cursor.md) — full catalog of\n    behavioural differences, removed APIs, signature-only changes, and a migration cheat sheet\n15. [**Reactive Sources**](docs/15.%20reactive-sources.md) — bridge Room/SQLDelight/Realm to a\n    paginator via `observe(...)` and `PaginatorReactiveCache` — point CRUD instead of `refreshAll`\n16. [Ask the author a question](https://t.me/+0eeAM-EJpqgwNGZi)\n\nMaintainer docs:\n\n- [**Releasing a New Version**](RELEASING.md) — publishing the library to Maven Central\n\n---\n\n## License\n\n```\nThe MIT License (MIT)\n\nCopyright (c) 2023 Jamal Aliev\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjamal-wia%2Fpaginator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjamal-wia%2Fpaginator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjamal-wia%2Fpaginator/lists"}