Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 37 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,14 @@ This is a Kotlin MultiPlatform library that contains pagination logic for kotlin
- [License](#license)

## Features
- **Pagination** implements pagination logic for the data from abstract `PagedListDataSource`.
- Managing a data loading process using **Pagination** asynchronous functions: `loadFirstPage`, `loadNextPage`,
`refresh` or their duplicates with `suspend` modifier.
- Observing states of **Pagination** using `LiveData` from **moko-mvvm**.
- **Pagination** implements pagination logic for the data from `PagingDataSource`.
- Managing data loading using `loadFirstPage`, `reloadFirstPage`, `loadNextPage`, `refresh`.
- Observing states using `StateFlow` and `RemoteState`.

## Requirements
- Gradle version 6.8+
- Gradle 8.10+
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why so highest version???

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supports AGP Version 8.6.1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

library must support a wide range of versions (e.g., ensuring compatibility with AGP versions older than 8.6.1)

- Android API 16+
- iOS version 11.0+
- iOS 11.0+

## Installation
root build.gradle
Expand All @@ -35,10 +34,11 @@ allprojects {
}
```

project build.gradle
```groovy
project build.gradle.kts
```kotlin
dependencies {
commonMainApi("dev.icerock.moko:paging:0.7.1")
commonMainApi("dev.icerock.moko:paging:0.8.0")
commonMainApi("dev.icerock.moko:remotestate:0.1.0")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's should be another lib at all

}
```

Expand All @@ -49,30 +49,23 @@ You can use **Pagination** in `commonMain` sourceset.
**Pagination** creation:

```kotlin
val pagination: Pagination<Int> = Pagination(
parentScope = coroutineScope,
dataSource = LambdaPagedListDataSource { currentList ->
extrenalRepository.loadPage(currentList)
},
comparator = Comparator { a: Int, b: Int ->
a - b
val pagination: Pagination<Item> = Pagination(
dataSource = PageSizePagingDataSource(
pageSize = 20,
calculateNextPage = { currentList ->
// your logic for calculating the next page
},
nextPageListener = { result: Result<List<Int>> ->
if (result.isSuccess) {
println("Next page successful loaded")
} else {
println("Next page loading failed")
}
},
refreshListener = { result: Result<List<Int>> ->
if (result.isSuccess) {
println("Refresh successful")
} else {
println("Refresh failed")
}
},
initValue = listOf(1, 2, 3)
)
loadPage = { page, pageSize -> repository.load(page = page, pageSize = pageSize) }
),
itemKey = { item -> item.id },
refreshStrategy = RefreshStrategy.ReplaceEverything,
nextPageListener = { result ->
result.onFailure { println("Next page loading failed: $it") }
},
refreshListener = { result ->
result.onFailure { println("Refresh failed: $it") }
}
)
```

Managing data loading:
Expand All @@ -94,19 +87,20 @@ pagination.setData(itemsList)
Observing **Pagination** states:

```kotlin
// Observing the state of the pagination
pagination.state.addObserver { state: ResourceState<List<ItemClass>, Throwable> ->
// ...
}
val state: RemoteState<PagingState<YourItem>, Throwable> by viewModel.pagination.state.collectAsState()

// Observing the next page loading process
pagination.nextPageLoading.addObserver { isLoading: Boolean ->
// ...
}
when (state) {
RemoteState.Loading -> {
// ...
}

// Observing the refresh process
pagination.refreshLoading.addObserver { isRefreshing: Boolean ->
// ...
is RemoteState.Error -> {
// ...
}

is RemoteState.Success<PagingState<YourItem>> -> {
// ...
}
}
```

Expand Down
3 changes: 0 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,8 @@ buildscript {
dependencies {
classpath(libs.kotlinGradlePlugin)
classpath(libs.androidGradlePlugin)
classpath(libs.googleServicesGradlePlugin)
classpath(libs.mokoGradlePlugin)
classpath(libs.mobileMultiplatformGradlePlugin)
classpath(libs.kotlinSerializationGradlePlugin)
classpath(libs.mokoUnitsGeneratorGradlePlugin)
}
}

Expand Down
7 changes: 2 additions & 5 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@ org.gradle.configureondemand=false
org.gradle.parallel=true

kotlin.code.style=official
kotlin.native.enableDependencyPropagation=false
kotlin.mpp.enableGranularSourceSetsMetadata=true
kotlin.mpp.enableCompatibilityMetadataVariant=true

android.useAndroidX=true

moko.android.targetSdk=31
moko.android.compileSdk=31
moko.android.targetSdk=35
moko.android.compileSdk=35
moko.android.minSdk=16

moko.publish.name=MOKO paging
Expand Down
49 changes: 22 additions & 27 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,38 +1,33 @@
[versions]
kotlinVersion = "1.6.20"
androidAppCompatVersion = "1.2.0"
androidLifecycleVersion = "2.1.0"
androidCoreTestingVersion = "2.1.0"
recyclerViewVersion = "1.1.0"
swipeRefreshLayoutVersion = "1.1.0"
ktorClientVersion = "2.0.0"
coroutinesVersion = "1.6.0-native-mt"
mokoMvvmVersion = "0.12.0"
mokoResourcesVersion = "0.18.0"
mokoUnitsVersion = "0.8.0"
mokoPagingVersion = "0.7.2"
kotlinVersion = "2.1.10"
lifecycleVersion = "2.8.7"
androidCoreTestingVersion = "2.2.0"
composeBomVersion = "2025.09.00"
activityComposeVersion = "1.10.1"
ktorClientVersion = "3.4.0"
coroutinesVersion = "1.10.2"
mokoMvvmVersion = "0.16.1"
mokoPagingVersion = "0.8.0"

[libraries]
appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" }
recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerViewVersion" }
lifecycle = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = "androidLifecycleVersion" }
swipeRefreshLayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swipeRefreshLayoutVersion" }
lifecycleViewModel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleVersion" }
lifecycleRuntimeCompose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleVersion" }
activityCompose = { module = "androidx.activity:activity-compose", version.ref = "activityComposeVersion" }
composeBom = { module = "androidx.compose:compose-bom", version.ref = "composeBomVersion" }
composeUi = { module = "androidx.compose.ui:ui" }
composeUiTooling = { module = "androidx.compose.ui:ui-tooling" }
composeUiToolingPreview = { module = "androidx.compose.ui:ui-tooling-preview" }
composeMaterial3 = { module = "androidx.compose.material3:material3" }
coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutinesVersion" }
coroutinesTest = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutinesVersion" }
ktorClient = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientVersion" }
mokoResources = { module = "dev.icerock.moko:resources", version.ref = "mokoResourcesVersion" }
mokoUnits = { module = "dev.icerock.moko:units", version.ref = "mokoUnitsVersion" }
mokoUnitsDataBinding = { module = "dev.icerock.moko:units-databinding", version.ref = "mokoUnitsVersion" }
mokoMvvmLiveData = { module = "dev.icerock.moko:mvvm-livedata", version.ref = "mokoMvvmVersion" }
mokoMvvmState = { module = "dev.icerock.moko:mvvm-state", version.ref = "mokoMvvmVersion" }
kotlinTestJUnit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlinVersion" }
mokoMvvmFlow = { module = "dev.icerock.moko:mvvm-flow", version.ref = "mokoMvvmVersion" }
androidCoreTesting = { module = "androidx.arch.core:core-testing", version.ref = "androidCoreTestingVersion" }
ktorClientMock = { module = "io.ktor:ktor-client-mock", version.ref = "ktorClientVersion" }

kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlinVersion" }
androidGradlePlugin = { module = "com.android.tools.build:gradle", version = "7.0.4" }
googleServicesGradlePlugin = { module = "com.google.gms:google-services", version = "4.3.8" }
firebaseGradlePlugin = { module = "com.google.firebase:firebase-crashlytics-gradle", version = "2.2.0" }
mokoGradlePlugin = { module = "dev.icerock.moko:moko-gradle-plugin", version = "0.1.0" }
mobileMultiplatformGradlePlugin = { module = "dev.icerock:mobile-multiplatform", version = "0.14.1" }
kotlinSerializationGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlinVersion" }
mokoUnitsGeneratorGradlePlugin = { module = "dev.icerock.moko:units-generator", version.ref = "mokoUnitsVersion" }
androidGradlePlugin = { module = "com.android.tools.build:gradle", version = "8.6.1" }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why so high?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All Compose dependencies (1.9.1) and lifecycle-runtime-compose (2.9.0) require AGP ≥ 8.6.0
Is this a big problem?

Copy link
Copy Markdown

@ExNDY ExNDY Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Project doesn't need AGP 8.6, 8.1 more comfortable version, you need separate sample and library versions

mokoGradlePlugin = { module = "dev.icerock.moko:moko-gradle-plugin", version = "0.6.0" }
mobileMultiplatformGradlePlugin = { module = "dev.icerock:mobile-multiplatform", version = "0.14.4" }
3 changes: 2 additions & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#Thu Feb 19 11:14:28 MSK 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
31 changes: 31 additions & 0 deletions paging-compose/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/

plugins {
id("dev.icerock.moko.gradle.multiplatform.mobile")
id("dev.icerock.moko.gradle.publication")
id("dev.icerock.moko.gradle.detekt")
id("org.jetbrains.kotlin.plugin.compose")
}

kotlin {
jvm()
}

android {
namespace = "dev.icerock.moko.paging.compose"

compileSdk = 35

defaultConfig {
minSdk = 21
}
}

dependencies {
commonMainApi(projects.paging)
implementation(libs.composeMaterial3)
androidMainImplementation(platform(libs.composeBom))
androidMainImplementation(libs.composeUi)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.icerock.moko.paging.compose

import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.lazy.LazyListLayoutInfo
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.PullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import dev.icerock.moko.paging.PagingState

/**
* Composable wrapper for paged content with pull-to-refresh and auto-load-next.
*
* @param state paging state with items and loading flags
* @param onLoadNextRequested callback for requesting the next page
* @param onRefresh callback for pull-to-refresh
* @param listState LazyListState used to detect proximity to the end
* @param pullToRefreshState PullToRefreshState for the indicator
* @param pullToRefreshIndicator composable for the pull-to-refresh indicator UI
* @param modifier modifier applied to the PullToRefreshBox
* @param content renders the list items
*/
@Suppress("LongMethod", "MagicNumber")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun <T> PagingContent(
state: PagingState<T>,
onLoadNextRequested: () -> Unit,
onRefresh: () -> Unit,
listState: LazyListState,
pullToRefreshState: PullToRefreshState,
pullToRefreshIndicator: @Composable BoxScope.() -> Unit,
modifier: Modifier = Modifier,
content: @Composable (List<T>) -> Unit,
) {
val shouldLoadMore: Boolean by remember {
derivedStateOf {
val layoutInfo: LazyListLayoutInfo = listState.layoutInfo
val lastVisibleIndex: Int? = layoutInfo.visibleItemsInfo.lastOrNull()?.index
val totalCount: Int = layoutInfo.totalItemsCount
val threshold: Int = PAGING_LOAD_THRESHOLD
val isCloseToEnd: Boolean = lastVisibleIndex != null &&
lastVisibleIndex >= totalCount - threshold
lastVisibleIndex != 0 && !state.isEndOfList && isCloseToEnd
}
}

LaunchedEffect(shouldLoadMore) {
if (shouldLoadMore && !state.isNextPageLoading) {
onLoadNextRequested()
}
}

PullToRefreshBox(
modifier = modifier,
isRefreshing = state.isRefreshing,
onRefresh = onRefresh,
state = pullToRefreshState,
indicator = pullToRefreshIndicator
) {
content.invoke(state.items)
}
}

private const val PAGING_LOAD_THRESHOLD: Int = 3
Loading