{"id":15140703,"url":"https://github.com/matt-ramotar/paging","last_synced_at":"2025-10-23T17:31:36.583Z","repository":{"id":227142459,"uuid":"770584788","full_name":"matt-ramotar/Paging","owner":"matt-ramotar","description":"📄 A paging library for large-scale Kotlin apps. Powered by Store, Molecule, and Compose Runtime","archived":false,"fork":false,"pushed_at":"2024-07-21T17:32:45.000Z","size":14851,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-30T19:47:43.987Z","etag":null,"topics":["compose-runtime","kotlin-multiplatform","molecule","paging","store"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/matt-ramotar.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}},"created_at":"2024-03-11T19:58:18.000Z","updated_at":"2024-12-15T23:22:44.000Z","dependencies_parsed_at":"2024-03-11T21:24:11.921Z","dependency_job_id":"e904bb2a-3701-4504-b25f-304876cda8af","html_url":"https://github.com/matt-ramotar/Paging","commit_stats":null,"previous_names":["matt-ramotar/paging"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/matt-ramotar%2FPaging","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/matt-ramotar%2FPaging/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/matt-ramotar%2FPaging/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/matt-ramotar%2FPaging/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/matt-ramotar","download_url":"https://codeload.github.com/matt-ramotar/Paging/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":237869019,"owners_count":19379246,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["compose-runtime","kotlin-multiplatform","molecule","paging","store"],"created_at":"2024-09-26T08:40:21.886Z","updated_at":"2025-10-23T17:31:36.000Z","avatar_url":"https://github.com/matt-ramotar.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 📄 StoreX Paging\n\n[![codecov](https://codecov.io/gh/matt-ramotar/Paging/graph/badge.svg?token=62YL5HZR9Q)](https://codecov.io/gh/matt-ramotar/Paging)\n\nA paging library for large-scale Kotlin applications. Powered by [Store](https://github.com/MobileNativeFoundation/Store), [Molecule](https://github.com/cashapp/molecule), and Compose Runtime.\n\n- **Performance**: This has been our highest priority. Preliminary performance comparisons are showing roughly the same load time and memory utilization as AndroidX Paging, but fewer recompositions\n  and janky frame delays.\n- **UDF-first design**: This core architecture makes state management clean and predictable. Related AndroidX Pager issue: https://issuetracker.google.com/issues/183495984.\n- **“Updating item” concept**: Each item in a list can update independently. Related AndroidX Pager issue: https://issuetracker.google.com/issues/160232968.\n- **Operation pipelines**: These operations are applied to ItemSnapshotList instances. This opens up on-the-fly data transformations, such as filtering, grouping, deduplicating, validating, or\n  enrichment. Related AndroidX Pager issue: https://issuetracker.google.com/issues/175430431.\n- **Local and remote mutations**: StoreX Paging handles both local and remote data changes.\n- **Customization options**: Many customization points including launch effects, middleware, side effects, eager loading strategies, fetching strategies, and error handling strategies.\n- **AndroidX Paging compatibility**: StoreX Paging is designed to be compatible with AndroidX Paging, facilitating easier migration or integration.\n- **Kotlin Multiplatform support**: Targeting all major Kotlin platforms.\n\n## [Circuit](https://github.com/slackhq/circuit) Sample\n\n### Getting Set Up\n\n1. Define your item ID, page request key, and item value types:\n\n```kotlin\nvalue class Cursor(val value: String)\n```\n\n```kotlin\ndata class TimelineRequest(\n    val cursor: Cursor?,\n    val size: Int,\n    val headers: Map\u003cString, String\u003e\n)\n```\n\n```kotlin\ndata class Post(\n    override val id: Cursor,\n    val title: String,\n    val body: String,\n    val authorId: String,\n    val createdAt: LocalDateTime,\n    val retweetCount: Int,\n    val favoriteCount: Int,\n    val commentCount: Int,\n    val relevanceScore: Float,\n    val trendingScore: Float,\n    val isLikedByUser: Boolean\n)\n```\n\n2. Implement an `IdExtractor`:\n\n```kotlin\nval idExtractor = IdExtractor\u003cCursor, Post\u003e { post -\u003e post.id }\n```\n\n3. Implement a `PagingSource`:\n\n```kotlin\nclass TimelinePagingSource : PagingSource\u003cCursor, TimelineRequest, Post\u003e {\n    override suspend fun load(params: PagingSource.LoadParams\u003cTimelineRequest\u003e):\n        PagingSource.LoadResult\u003cCursor, TimelineRequest, Post\u003e {\n        TODO(\"Fetch posts from network\")\n    }\n}\n```\n\n4. Build a `Pager`:\n\n```kotlin\nPager\n    .builder\u003cCursor, TimelineRequest, Post\u003e(pagingConfig)\n    .setIdExtractor(idExtractor)\n    .setPagingSource(TimelinePagingSource())\n    .addLaunchEffects(launchEffect1, launchEffectN)\n    .addSideEffects(sideEffect1, sideEffectN)\n    .addMiddleware(middleware1, middlewareN)\n    .addFetchingStrategy(customFetchingStrategy)\n    .addErrorHandlingStrategy(customErrorHandlingStrategy)\n    .build()\n```\n\n5. Provide the `Pager` to the compose environment at the appropriate level in the composition hierarchy:\n\n```kotlin\nclass TimelineScreenUi(private val pager: Pager) : Ui\u003cTimelineScreen.State\u003e {\n    @Composable\n    override fun Content(state: TimelineScreen.State, modifier: Modifier) {\n        PagingScope(pager) {\n            LazyUpdatingItems(state.ids, modifier) { model: UpdatingItem\u003cCursor, Post\u003e -\u003e\n                TimelinePostUi(model)\n            }\n        }\n    }\n}\n```\n\n6. Provide `PagingState` to the appropriate composable UI:\n\n```kotlin\nclass TimelineScreenPresenter(\n    private val pager: Pager\u003cCursor\u003e,\n) : Presenter\u003cTimelineScreen.State\u003e {\n    @Composable\n    override fun present(): TimelineScreen.State {\n        val pagingState = pager.collectAsState()\n        return TimelineScreen.State(\n            ids = pagingState.ids,\n            eventSink = { event -\u003e\n                when (event) {\n                    TimelineScreen.Event.Refresh -\u003e pager.dispatch(PagingAction.refresh())\n                }\n            }\n        )\n    }\n}\n```\n\n### Adding Sorting and Filtering\n\n1. Implement the `Operation` abstract class for customized sorting and filtering:\n\n```kotlin\nclass SortForTimeRange(private val timeRange: TimeRange) :\n    Operation\u003cCursor, TimelineRequest, Post\u003e() {\n    internal override fun shouldApply(\n        key: TimelineRequest?,\n        pagingState: PagingState\u003cCursor\u003e,\n        fetchingState: FetchingState\u003cCursor, TimelineRequest\u003e\n    ): Boolean {\n        // Always apply\n        return true\n    }\n\n    internal override fun apply(\n        snapshot: ItemSnapshotList\u003cCursor, Post\u003e,\n        key: TimelineRequest?,\n        pagingState: PagingState\u003cCursor\u003e,\n        fetchingState: FetchingState\u003cCursor, TimelineRequest\u003e\n    ): ItemSnapshotList\u003cCursor, Post\u003e {\n        val now = Clock.System.now().toLocalDateTime(TimeZone.UTC)\n\n        // Separate loaded items and placeholders\n        val (loadedItems, placeholders) = snapshot.partition { it != null }\n\n        // Sort and filter loaded items\n        val sortedItems = loadedItems.filterNotNull()\n            .filter { post -\u003e isWithinRange(post.createdAt, timeRange, now) }\n            .sortedWith(\n                compareByDescending\u003cPost\u003e { it.favoriteCount }.thenByDescending { it.createdAt }\n            )\n\n        // Combine sorted items with placeholders at the end\n        val result = sortedItems + placeholders\n        return ItemSnapshotList(result)\n    }\n\n    private fun isWithinRange(\n        createdAt: LocalDateTime,\n        timeRange: TimeRange,\n        now: LocalDateTime\n    ): Boolean {\n        val durationSinceCreation = now.toInstant(TimeZone.UTC) - createdAt.toInstant(TimeZone.UTC)\n        return durationSinceCreation \u003c timeRange.duration\n    }\n}\n```\n\n2. Update the `OperationPipeline` based on user configuration\n\n```kotlin\nclass TimelineScreenPresenter(...) : Presenter\u003cTimelineScreen.State\u003e {\n    @Composable\n    override fun present(): TimelineScreen.State {\n        val pagingState = pager.collectAsState()\n        var sortingMethod by remember { mutableStateOf\u003cSortingMethod\u003e(SortingMethod.New) }\n\n        LaunchedEffect(sortingMethod) {\n            val operation = when (sortingMethod) {\n                is Top -\u003e SortForTimeRange(operation.timeRange)\n            }\n            pager.dispatch(PagingAction.UpdateOperations(operation))\n        }\n\n        return TimelineScreen.State(\n            ids = pagingState.ids,\n            eventSink = { event -\u003e\n                when (event) {\n                    TimelineScreen.Event.Refresh -\u003e pager.dispatch(PagingAction.refresh())\n                    TimelineEvent.UpdateSort -\u003e sortingMethod = event.sortingMethod\n                }\n            }\n        )\n    }\n}\n```\n\n### Adding Mutations\n\n```kotlin\n@Composable\nfun TimelinePostUi(model: UpdatingItem\u003cCursor, Post\u003e) {\n    val coroutineScope = rememberCoroutineScope()\n    val state = model.collectAsState(coroutineScope)\n    val post = state.value.item\n\n    if (post == null) {\n        TimelinePostPlaceholderUi()\n    } else {\n        TimelinePostLoadedUi(\n            post = post,\n            updatePost = { updatedPost -\u003e model.emit(UpdatingItem.Event.Update(updatedPost)) }\n        )\n    }\n}\n\n@Composable\nfun TimelinePostLoadedUi(\n    post: Post,\n    updatePost: (next: Post) -\u003e Unit\n) {\n    val isLikedByUser by remember { derivedStateOf { post.isLikedByUser } }\n\n    Column {\n        // ...\n\n        LikeAction(\n            isLikedByUser = isLikedByUser,\n            onClick = { updatePost(post.copy(isLikedByUser = !isLikedByUser)) }\n        )\n    }\n}\n```\n\n### Using a Custom Comparator\n\n```kotlin\n\nobject CursorComparator : Comparator\u003cCursor\u003e {\n\n    override fun compare(a: Cursor, b: Cursor): Int {\n        val parsedCursorA = parseCursor(a)\n        val parsedCursorB = parseCursor(b)\n        return parsedCursorA.first.compareTo(parsedCursorB.first)\n    }\n\n    override fun distance(a: Cursor, b: Cursor): Int {\n        val parsedCursorA = parseCursor(a)\n        val parsedCursorB = parseCursor(b)\n\n        // Compare timestamps\n        val timeDiff = parsedCursorA.first - parsedCursorB.first\n\n        return when {\n            // If timestamps are different, use their difference\n            // Coercing to Int range to avoid overflow issues\n            timeDiff != 0L -\u003e timeDiff.coerceIn(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong()).toInt()\n\n            // If timestamps are the same, compare the unique parts lexicographically\n            // This ensures a consistent, deterministic ordering\n            else -\u003e parsedCursorA.second.compareTo(parsedCursorB.second)\n        }\n    }\n\n    private fun parseCursor(cursor: Cursor): Pair\u003cLong, String\u003e {\n        // Parsing the cursor string into its components\n        val parts = cursor.split('-')\n        require(parts.size == 2) { \"Invalid cursor format. Expected 'timestamp-uniqueId'\" }\n\n        // Converting the timestamp string to a Long for numerical comparison\n        val timestamp = parts[0].toLongOrNull() ?: throw IllegalArgumentException(\"Invalid timestamp in cursor\")\n        val uniqueId = parts[1]\n\n        return timestamp to uniqueId\n    }\n}\n```\n\n## Contributing\n\nReach out at https://kotlinlang.slack.com/archives/C06007Z01HU\n\n## License\n\n```txt\nCopyright 2024 Mobile Native Foundation\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n   http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmatt-ramotar%2Fpaging","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmatt-ramotar%2Fpaging","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmatt-ramotar%2Fpaging/lists"}