├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── themes.xml
│ │ │ │ └── colors.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── xml
│ │ │ │ ├── backup_rules.xml
│ │ │ │ └── data_extraction_rules.xml
│ │ │ └── drawable
│ │ │ │ ├── ic_launcher_foreground.xml
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── jamal_aliev
│ │ │ │ └── paginator
│ │ │ │ ├── MainViewState.kt
│ │ │ │ ├── ui
│ │ │ │ └── theme
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ ├── Type.kt
│ │ │ │ │ └── Theme.kt
│ │ │ │ ├── SampleRepository.kt
│ │ │ │ ├── MainViewModel.kt
│ │ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── jamal_aliev
│ │ │ └── paginator
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── jamal_aliev
│ │ └── paginator
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle.kts
├── paginator
├── .gitignore
├── consumer-rules.pro
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── jamal_aliev
│ │ │ └── paginator
│ │ │ ├── bookmark
│ │ │ └── Bookmark.kt
│ │ │ ├── initializer
│ │ │ └── InitializerTypealias.kt
│ │ │ ├── exception
│ │ │ └── LockedException.kt
│ │ │ ├── page
│ │ │ └── PageState.kt
│ │ │ ├── extension
│ │ │ ├── PageExt.kt
│ │ │ └── PaginatorExt.kt
│ │ │ └── MutablePaginator.kt
│ ├── androidTest
│ │ └── java
│ │ │ └── com
│ │ │ └── jamal_aliev
│ │ │ └── paginator
│ │ │ └── ExampleInstrumentedTest.kt
│ └── test
│ │ └── java
│ │ └── com
│ │ └── jamal_aliev
│ │ └── paginator
│ │ ├── SmartForEachTest.kt
│ │ ├── pagestate
│ │ ├── ProgressPageStateTest.kt
│ │ ├── EmptyPageStateTest.kt
│ │ ├── ErrorPageStateTest.kt
│ │ └── SuccessPageStateTest.kt
│ │ └── MutablePaginatorTest.kt
├── proguard-rules.pro
└── build.gradle.kts
├── jitpack.yml
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .idea
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
└── .gitignore
├── .gitignore
├── settings.gradle.kts
├── LICENSE
├── gradle.properties
├── gradlew.bat
├── README.md
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/paginator/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/paginator/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/jitpack.yml:
--------------------------------------------------------------------------------
1 | jdk:
2 | - openjdk17
3 | before_install:
4 | - ./scripts/prepareJitpackEnvironment.sh
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Paginator
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamal-wia/Paginator/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamal-wia/Paginator/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamal-wia/Paginator/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamal-wia/Paginator/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamal-wia/Paginator/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamal-wia/Paginator/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamal-wia/Paginator/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamal-wia/Paginator/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamal-wia/Paginator/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamal-wia/Paginator/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamal-wia/Paginator/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/paginator/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | /vcs.xml
5 | /misc.xml
6 | /migrations.xml
7 | /kotlinc.xml
8 | /gradle.xml
9 | /deploymentTargetDropDown.xml
10 | /compiler.xml
11 | /inspectionProfiles/Project_Default.xml
12 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Jan 14 03:00:01 TRT 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/paginator/src/main/java/com/jamal_aliev/paginator/bookmark/Bookmark.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator.bookmark
2 |
3 | interface Bookmark {
4 |
5 | val page: UInt
6 |
7 | @JvmInline
8 | value class BookmarkUInt(
9 | override val page: UInt
10 | ) : Bookmark
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jamal_aliev/paginator/MainViewState.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator
2 |
3 | import com.jamal_aliev.paginator.page.PageState
4 |
5 | sealed class MainViewState {
6 |
7 | data class DataState(
8 | val data: List>
9 | ) : MainViewState()
10 |
11 | data object ProgressState : MainViewState()
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jamal_aliev/paginator/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/test/java/com/jamal_aliev/paginator/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | .cxx/
4 | .externalNativeBuild/
5 | .gradle/
6 | .idea/*
7 | !.idea/fileTemplates/
8 | !.idea/inspectionProfiles/
9 | !.idea/codeStyles/
10 | .tander/
11 | build/
12 | captures/
13 | fastlane/report.xml
14 | fastlane/README.md
15 |
16 | *.apk
17 | *.hprof
18 | *.iml
19 | *.jks
20 | *.keystore
21 | *.log
22 | deploymentTargetDropDown.xml
23 | google-services.json
24 | keystore.properties
25 | local.properties
26 | misc.xml
27 | output.json
28 | render.experimental.xml
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jamal_aliev/paginator/SampleRepository.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator
2 |
3 | import kotlinx.coroutines.delay
4 |
5 | object SampleRepository {
6 |
7 | private const val PAGE_SIZE = 20
8 |
9 | private val data = List(10_000) { "$it" }
10 |
11 | /**
12 | *The function generates an error with a probability of 50 percent
13 | * */
14 | suspend fun loadPage(page: Int): List {
15 | delay(timeMillis = (1000L..3000L).random())
16 | return if (Math.random() < 0.5) {
17 | throw Exception("error")
18 | } else {
19 | data.subList(PAGE_SIZE * (page - 1), PAGE_SIZE * page)
20 | }
21 | }
22 |
23 | }
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | maven { setUrl("https://jitpack.io") }
20 | }
21 | }
22 |
23 | rootProject.name = "Paginator"
24 | include(":app")
25 | include(":paginator")
26 |
--------------------------------------------------------------------------------
/paginator/src/main/java/com/jamal_aliev/paginator/initializer/InitializerTypealias.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator.initializer
2 |
3 | import com.jamal_aliev.paginator.page.PageState.EmptyPage
4 | import com.jamal_aliev.paginator.page.PageState.ErrorPage
5 | import com.jamal_aliev.paginator.page.PageState.ProgressPage
6 | import com.jamal_aliev.paginator.page.PageState.SuccessPage
7 |
8 | internal typealias InitializerEmptyPage = (page: UInt, data: List) -> EmptyPage
9 |
10 | internal typealias InitializerErrorPage = (e: Exception, page: UInt, data: List) -> ErrorPage
11 |
12 | internal typealias InitializerProgressPage = (page: UInt, data: List) -> ProgressPage
13 |
14 | internal typealias InitializerSuccessPage = (page: UInt, data: List) -> SuccessPage
15 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/jamal_aliev/paginator/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.jamal_aliev.paginator", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/paginator/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/paginator/src/androidTest/java/com/jamal_aliev/paginator/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.jamal_aliev.paginator.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/paginator/src/main/java/com/jamal_aliev/paginator/exception/LockedException.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator.exception
2 |
3 | sealed class LockedException(
4 | override val message: String?
5 | ) : Exception(message) {
6 |
7 | open class JumpWasLockedException : LockedException(
8 | message = "Jump was locked. Please try set false to field lockJump"
9 | )
10 |
11 | open class GoNextPageWasLockedException : LockedException(
12 | message = "NextPage was locked. Please try set false to field lockGoNextPage"
13 | )
14 |
15 | open class GoPreviousPageWasLockedException : LockedException(
16 | message = "PreviousPage was locked. Please try set false to field lockGoPreviousPage"
17 | )
18 |
19 | open class RestartWasLockedException : LockedException(
20 | message = "Restart was locked. Please try set false to field lockRestart"
21 | )
22 |
23 | open class RefreshWasLockedException : LockedException(
24 | message = "Refresh was locked. Please try set false to field lockRefresh"
25 | )
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2023 Jamal Aliev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
15 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jamal_aliev/paginator/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/paginator/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | id("org.jetbrains.kotlin.android")
4 | id("maven-publish")
5 | }
6 |
7 | group = "com.github.jamal-wia"
8 | version = "5.0.0"
9 |
10 | android {
11 | namespace = "com.jamal_aliev.paginator"
12 | compileSdk = 36
13 |
14 | defaultConfig {
15 | minSdk = 24
16 |
17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
18 | consumerProguardFiles("consumer-rules.pro")
19 | }
20 |
21 | buildTypes {
22 | release {
23 | isMinifyEnabled = false
24 | proguardFiles(
25 | getDefaultProguardFile("proguard-android-optimize.txt"),
26 | "proguard-rules.pro"
27 | )
28 | }
29 | }
30 | compileOptions {
31 | sourceCompatibility = JavaVersion.VERSION_1_8
32 | targetCompatibility = JavaVersion.VERSION_1_8
33 | }
34 | kotlinOptions {
35 | jvmTarget = JavaVersion.VERSION_1_8.toString()
36 | }
37 | }
38 |
39 | dependencies {
40 |
41 | implementation("androidx.core:core-ktx:1.17.0")
42 | implementation("androidx.appcompat:appcompat:1.7.1")
43 | implementation("com.google.android.material:material:1.13.0")
44 |
45 | testImplementation("junit:junit:4.13.2")
46 | androidTestImplementation("androidx.test.ext:junit:1.3.0")
47 | androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
48 |
49 | }
50 |
51 | afterEvaluate {
52 | publishing {
53 | publications {
54 | create("release", MavenPublication::class) {
55 | from(components["release"])
56 | groupId = "com.github.jamal-wia"
57 | artifactId = "paginator"
58 | version = "5.0.0"
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jamal_aliev/paginator/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.SideEffect
13 | import androidx.compose.ui.graphics.toArgb
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.platform.LocalView
16 | import androidx.core.view.WindowCompat
17 |
18 | private val DarkColorScheme = darkColorScheme(
19 | primary = Purple80,
20 | secondary = PurpleGrey80,
21 | tertiary = Pink80
22 | )
23 |
24 | private val LightColorScheme = lightColorScheme(
25 | primary = Purple40,
26 | secondary = PurpleGrey40,
27 | tertiary = Pink40
28 |
29 | /* Other default colors to override
30 | background = Color(0xFFFFFBFE),
31 | surface = Color(0xFFFFFBFE),
32 | onPrimary = Color.White,
33 | onSecondary = Color.White,
34 | onTertiary = Color.White,
35 | onBackground = Color(0xFF1C1B1F),
36 | onSurface = Color(0xFF1C1B1F),
37 | */
38 | )
39 |
40 | @Composable
41 | fun PaginatorTheme(
42 | darkTheme: Boolean = isSystemInDarkTheme(),
43 | // Dynamic color is available on Android 12+
44 | dynamicColor: Boolean = true,
45 | content: @Composable () -> Unit
46 | ) {
47 | val colorScheme = when {
48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
49 | val context = LocalContext.current
50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
51 | }
52 |
53 | darkTheme -> DarkColorScheme
54 | else -> LightColorScheme
55 | }
56 | val view = LocalView.current
57 | if (!view.isInEditMode) {
58 | SideEffect {
59 | val window = (view.context as Activity).window
60 | window.statusBarColor = colorScheme.primary.toArgb()
61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
62 | }
63 | }
64 |
65 | MaterialTheme(
66 | colorScheme = colorScheme,
67 | typography = Typography,
68 | content = content
69 | )
70 | }
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("org.jetbrains.kotlin.android")
4 | id("org.jetbrains.kotlin.plugin.compose")
5 | }
6 |
7 | android {
8 | namespace = "com.jamal_aliev.paginator"
9 | compileSdk = 36
10 |
11 | defaultConfig {
12 | applicationId = "com.jamal_aliev.paginator"
13 | minSdk = 24
14 | targetSdk = 36
15 | versionCode = 1
16 | versionName = "1.0"
17 |
18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
19 | vectorDrawables {
20 | useSupportLibrary = true
21 | }
22 | }
23 |
24 |
25 | buildTypes {
26 | release {
27 | isMinifyEnabled = false
28 | proguardFiles(
29 | getDefaultProguardFile("proguard-android-optimize.txt"),
30 | "proguard-rules.pro"
31 | )
32 | }
33 | }
34 | compileOptions {
35 | sourceCompatibility = JavaVersion.VERSION_1_8
36 | targetCompatibility = JavaVersion.VERSION_1_8
37 | }
38 | kotlinOptions {
39 | jvmTarget = "1.8"
40 | }
41 | buildFeatures {
42 | compose = true
43 | }
44 | packaging {
45 | resources {
46 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
47 | }
48 | }
49 | }
50 |
51 | dependencies {
52 |
53 | implementation("androidx.core:core-ktx:1.17.0")
54 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
55 |
56 | // UI - Compose
57 | implementation(platform("androidx.compose:compose-bom:2025.12.00"))
58 | implementation("androidx.compose.runtime:runtime:1.10.0")
59 | androidTestImplementation(platform("androidx.compose:compose-bom:2025.12.00"))
60 | implementation("androidx.compose.ui:ui")
61 | implementation("androidx.compose.ui:ui-graphics")
62 | implementation("androidx.compose.material3:material3")
63 | implementation("androidx.compose.runtime:runtime-livedata")
64 | implementation("androidx.lifecycle:lifecycle-runtime-compose")
65 | implementation("androidx.lifecycle:lifecycle-viewmodel-compose")
66 | implementation("androidx.activity:activity-compose:1.12.1")
67 | implementation("androidx.compose.ui:ui-tooling-preview")
68 | implementation("io.coil-kt:coil-compose:2.7.0")
69 | debugImplementation("androidx.compose.ui:ui-tooling")
70 | debugImplementation("androidx.compose.ui:ui-tooling")
71 | debugImplementation("androidx.compose.ui:ui-test-manifest")
72 |
73 | implementation(project(":paginator"))
74 |
75 | testImplementation("junit:junit:4.13.2")
76 | androidTestImplementation("androidx.test.ext:junit:1.3.0")
77 | androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
78 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jamal_aliev/paginator/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.ViewModelProvider
5 | import androidx.lifecycle.viewModelScope
6 | import com.jamal_aliev.paginator.MainViewState.DataState
7 | import com.jamal_aliev.paginator.MainViewState.ProgressState
8 | import com.jamal_aliev.paginator.page.PageState
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.async
11 | import kotlinx.coroutines.flow.MutableStateFlow
12 | import kotlinx.coroutines.flow.asStateFlow
13 | import kotlinx.coroutines.flow.filter
14 | import kotlinx.coroutines.flow.flowOn
15 | import kotlinx.coroutines.flow.launchIn
16 | import kotlinx.coroutines.flow.onEach
17 | import kotlinx.coroutines.flow.update
18 | import kotlinx.coroutines.launch
19 |
20 | class MainViewModel : ViewModel() {
21 |
22 | private val _state: MutableStateFlow = MutableStateFlow(ProgressState)
23 | val state = _state.asStateFlow()
24 |
25 | private val paginator = MutablePaginator(source = { SampleRepository.loadPage(it.toInt()) })
26 |
27 | init {
28 | paginator.snapshot
29 | .filter { it.isNotEmpty() }
30 | .onEach { data -> _state.update { DataState(data) } }
31 | .flowOn(Dispatchers.Main)
32 | .launchIn(viewModelScope)
33 |
34 | viewModelScope.launch {
35 | val async1 = async { paginator.loadOrGetPageState(1u, forceLoading = true) }
36 | val async2 = async { paginator.loadOrGetPageState(2u, forceLoading = true) }
37 | val async3 = async { paginator.loadOrGetPageState(3u, forceLoading = true) }
38 | val pageState1 = async1.await()
39 | val pageState2 = async2.await()
40 | val pageState3 = async3.await()
41 | paginator.setState(state = pageState1, silently = true)
42 | paginator.setState(state = pageState2, silently = true)
43 | paginator.setState(state = pageState3, silently = true)
44 | paginator.jumpForward()
45 | }
46 | }
47 |
48 | // 1,2,3 ... 11,12,13
49 | // 0,1,2 3 ,4 ,5
50 |
51 | //
52 |
53 |
54 | fun endReached() {
55 | viewModelScope.launch {
56 | paginator.goNextPage()
57 | }
58 | }
59 |
60 | fun refreshPage(pageState: PageState.ErrorPage) {
61 | viewModelScope.launch {
62 | paginator.refresh(pages = listOf(pageState.page))
63 | }
64 | }
65 |
66 | override fun onCleared() {
67 | paginator.release()
68 | super.onCleared()
69 | }
70 |
71 | class Factory : ViewModelProvider.Factory {
72 | override fun create(modelClass: Class): T {
73 | return MainViewModel() as T
74 | }
75 | }
76 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/paginator/src/main/java/com/jamal_aliev/paginator/page/PageState.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator.page
2 |
3 | import java.util.concurrent.atomic.AtomicLong
4 |
5 | sealed class PageState(
6 | open val page: UInt,
7 | open val data: List,
8 | open val id: Long = ids.incrementAndGet(),
9 | ) : Comparable> {
10 |
11 | abstract fun copy(
12 | page: UInt = this.page,
13 | data: List = this.data,
14 | id: Long = this.id
15 | ): PageState
16 |
17 | override fun toString() = "${this::class.simpleName}(page=$page id=$id data=$data)"
18 |
19 | override fun hashCode(): Int = this.page.hashCode()
20 |
21 | override fun equals(other: Any?): Boolean = (other as? PageState<*>)?.id == id
22 |
23 | override operator fun compareTo(other: PageState<*>): Int = page.compareTo(other.page)
24 |
25 | open class ErrorPage(
26 | val exception: Exception,
27 | override val page: UInt,
28 | override val data: List,
29 | override val id: Long = ids.incrementAndGet(),
30 | ) : PageState(page, data, id) {
31 |
32 | open fun copy(
33 | exception: Exception,
34 | page: UInt,
35 | data: List,
36 | id: Long = this.id
37 | ): ErrorPage {
38 | return ErrorPage(exception, page, data, id)
39 | }
40 |
41 | override fun copy(page: UInt, data: List, id: Long): ErrorPage {
42 | return copy(this.exception, page, data, id)
43 | }
44 |
45 | override fun toString(): String {
46 | return "${this::class.simpleName}(exception=${exception}, data=${this.data})"
47 | }
48 | }
49 |
50 | open class ProgressPage(
51 | override val page: UInt,
52 | override val data: List,
53 | override val id: Long = ids.incrementAndGet()
54 | ) : PageState(page, data, id) {
55 |
56 | override fun copy(page: UInt, data: List, id: Long) = ProgressPage(page, data, id)
57 | }
58 |
59 | open class SuccessPage(
60 | override val page: UInt,
61 | override val data: List,
62 | override val id: Long = ids.incrementAndGet()
63 | ) : PageState(page, data, id) {
64 |
65 | init {
66 | checkData()
67 | }
68 |
69 | @Suppress("NOTHING_TO_INLINE")
70 | private inline fun checkData() {
71 | if (this !is EmptyPage) {
72 | require(data.isNotEmpty()) { "data must not be empty" }
73 | }
74 | }
75 |
76 | /**
77 | * If you want to override this function, you should check the data because it can't be empty
78 | * */
79 | override fun copy(page: UInt, data: List, id: Long): SuccessPage {
80 | return if (data.isEmpty()) EmptyPage(page, data, id)
81 | else SuccessPage(page, data, id)
82 | }
83 | }
84 |
85 | open class EmptyPage(
86 | override val page: UInt,
87 | override val data: List,
88 | override val id: Long = ids.incrementAndGet()
89 | ) : SuccessPage(page, data, id) {
90 |
91 | override fun copy(page: UInt, data: List, id: Long) = EmptyPage(page, data, id)
92 | }
93 |
94 | private companion object {
95 | private var ids = AtomicLong(0L)
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # **Paginator**
2 |
3 | [](https://jitpack.io/#jamal-wia/Paginator) [](https://opensource.org/licenses/MIT)
4 |
5 | **Paginator** - это современная библеотека для реализации пагинации в вашем Android приложении,
6 | в которой реализованно моножество сложных кейсов таких как: "прыжки", загрузка следующих и
7 | предыдущих страниц,
8 | множественные источники данных, предварительная загрузка данных и удобное API для взаимодействия
9 | [Telegram Paginator_Library](https://t.me/+0eeAM-EJpqgwNGZi)
10 | [YouTube Tutorial RU](https://www.youtube.com/watch?v=YsUX7-FgKgA)
11 |
12 | ## **Подключение**
13 |
14 | ``` Gradle
15 | repositories {
16 | ....
17 | maven { setUrl("https://jitpack.io") }
18 | }
19 | ```
20 |
21 | ``` Gradle
22 | implementation("com.github.jamal-wia:Paginator:5.0.0")
23 | ```
24 |
25 | ## **Быстрый старт в 3 шага**
26 |
27 | ### Шаг 1
28 |
29 | Вам необходимо создать объект Paginator в вашем Presenter or ViewModel. И указать основной источник
30 | данных (REST API, Database)
31 |
32 | #### Пример кода
33 |
34 | ``` Kotlin
35 | class MainViewModel : ViewModel() {
36 |
37 | private val paginator = Paginator { SampleRepository.loadPage(it.toInt()) }
38 |
39 | }
40 | ```
41 |
42 | ### Шаг 2
43 |
44 | Далее вам необходимо "прыгнуть" на определенную "закладку" (по умолчанию это первая страница)
45 |
46 | #### Пример кода
47 |
48 | ``` Kotlin
49 | class MainViewModel : ViewModel() {
50 |
51 | private val paginator = Paginator { SampleRepository.loadPage(it.toInt()) }
52 |
53 | init {
54 | viewModelScope.launch {
55 | // асинхронная загрузка сразу нескольких страничек (зарание)
56 | val async1 = async { paginator.loadPageState(1u) } // опционально
57 | val async2 = async { paginator.loadPageState(2u) } // опционально
58 | val async3 = async { paginator.loadPageState(3u) } // опционально
59 | paginator.setPageState(page = 1u, async1.await()) // опционально
60 | paginator.setPageState(page = 2u, async2.await()) // опционально
61 | paginator.setPageState(page = 3u, async3.await()) // опционально
62 | paginator.jumpForward() // "прыжок" на "закладку" (первую страницу)
63 | }
64 | }
65 | }
66 | ```
67 |
68 |
69 | ### Шаг 3
70 |
71 | После того как вам больше не нужен инстанс Paginator'a вы должны вызывать метод очистки
72 |
73 | #### Пример кода
74 |
75 | ``` Kotlin
76 | class MainViewModel : ViewModel() {
77 |
78 | private val paginator = Paginator { SampleRepository.loadPage(it.toInt()) }
79 |
80 | override fun onCleared() {
81 | paginator.release()
82 | super.onCleared()
83 | }
84 | }
85 | ```
86 |
87 | ## License
88 |
89 | ```
90 | The MIT License (MIT)
91 |
92 | Copyright (c) 2023 Jamal Aliev
93 |
94 | Permission is hereby granted, free of charge, to any person obtaining a copy
95 | of this software and associated documentation files (the "Software"), to deal
96 | in the Software without restriction, including without limitation the rights
97 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
98 | copies of the Software, and to permit persons to whom the Software is
99 | furnished to do so, subject to the following conditions:
100 |
101 | The above copyright notice and this permission notice shall be included in all
102 | copies or substantial portions of the Software.
103 |
104 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
105 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
106 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
107 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
108 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
109 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
110 | SOFTWARE.
111 | ```
112 |
--------------------------------------------------------------------------------
/paginator/src/test/java/com/jamal_aliev/paginator/SmartForEachTest.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator
2 |
3 | import com.jamal_aliev.paginator.extension.smartForEach
4 | import com.jamal_aliev.paginator.page.PageState
5 | import com.jamal_aliev.paginator.page.PageState.EmptyPage
6 | import com.jamal_aliev.paginator.page.PageState.ErrorPage
7 | import com.jamal_aliev.paginator.page.PageState.ProgressPage
8 | import com.jamal_aliev.paginator.page.PageState.SuccessPage
9 | import junit.framework.TestCase.assertEquals
10 | import junit.framework.TestCase.assertTrue
11 | import org.junit.Test
12 |
13 | class SmartForEachTest {
14 |
15 | @Test
16 | fun `test smartForEach default forward iteration`() {
17 | val paginator = createPaginatorWith(5)
18 | val visited = mutableListOf()
19 |
20 | paginator.smartForEach { _, index, _ ->
21 | visited += index
22 | true
23 | }
24 |
25 | assertEquals(listOf(0, 1, 2, 3, 4), visited)
26 | }
27 |
28 | @Test
29 | fun `test smartForEach custom initial index`() {
30 | val paginator = createPaginatorWith(5)
31 | val visited = mutableListOf()
32 |
33 | paginator.smartForEach(
34 | initialIndex = { 2 }, // start from index 2
35 | ) { _, index, _ ->
36 | visited += index
37 | true
38 | }
39 |
40 | assertEquals(listOf(2, 3, 4), visited)
41 | }
42 |
43 | @Test
44 | fun `test smartForEach backward iteration`() {
45 | val paginator = createPaginatorWith(5)
46 | val visited = mutableListOf()
47 |
48 | paginator.smartForEach(
49 | initialIndex = { it.lastIndex },
50 | step = { it - 1 }
51 | ) { _, index, _ ->
52 | visited += index
53 | true
54 | }
55 |
56 | assertEquals(listOf(4, 3, 2, 1, 0), visited)
57 | }
58 |
59 | @Test
60 | fun `test smartForEach skipping elements`() {
61 | val paginator = createPaginatorWith(6)
62 | val visited = mutableListOf()
63 |
64 | paginator.smartForEach(
65 | step = { it + 2 } // step = 2
66 | ) { _, index, _ ->
67 | visited += index
68 | true
69 | }
70 |
71 | assertEquals(listOf(0, 2, 4), visited)
72 | }
73 |
74 | @Test
75 | fun `test smartForEach early stop`() {
76 | val paginator = createPaginatorWith(10)
77 | val visited = mutableListOf()
78 |
79 | paginator.smartForEach { _, index, _ ->
80 | visited += index
81 | index < 3 // stop after index == 3
82 | }
83 |
84 | assertEquals(listOf(0, 1, 2, 3), visited)
85 | }
86 |
87 | @Test
88 | fun `test smartForEach initial index out of bounds`() {
89 | val paginator = createPaginatorWith(5)
90 | val visited = mutableListOf()
91 |
92 | paginator.smartForEach(
93 | initialIndex = { 10 } // invalid index
94 | ) { _, index, _ ->
95 | visited += index
96 | true
97 | }
98 |
99 | assertTrue(visited.isEmpty())
100 | }
101 |
102 | @Test
103 | fun `test smartForEach returns equals list`() {
104 | val paginator = createPaginatorWith(3)
105 | val result = paginator.smartForEach { _, _, _ -> true }
106 |
107 | assertEquals(paginator.pageStates, result)
108 | }
109 | }
110 |
111 | private fun createPaginatorWith(n: Int): MutablePaginator {
112 | val paginator = MutablePaginator { emptyList() }
113 | repeat(n) { index ->
114 | paginator.setState(
115 | createRandomPageState(page = index.toUInt(), data = listOf("data $index")),
116 | silently = true
117 | )
118 | }
119 | return paginator
120 | }
121 |
122 | private fun createRandomPageState(page: UInt, data: List): PageState {
123 | return when ((0..100).random()) {
124 | in 0..24 -> ProgressPage(page, data)
125 | in 25..49 -> EmptyPage(page, data)
126 | in 50..75 -> ErrorPage(Exception(), page, data)
127 | else -> SuccessPage(page, data)
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/paginator/src/test/java/com/jamal_aliev/paginator/pagestate/ProgressPageStateTest.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator.pagestate
2 |
3 | import com.jamal_aliev.paginator.extension.isProgressState
4 | import com.jamal_aliev.paginator.extension.isRealProgressState
5 | import com.jamal_aliev.paginator.page.PageState
6 | import com.jamal_aliev.paginator.page.PageState.ProgressPage
7 | import org.junit.Assert.assertEquals
8 | import org.junit.Assert.assertFalse
9 | import org.junit.Assert.assertNotEquals
10 | import org.junit.Assert.assertNotSame
11 | import org.junit.Assert.assertTrue
12 | import org.junit.Test
13 | import kotlin.random.Random
14 |
15 | class ProgressPageStateTest {
16 |
17 | @Test
18 | fun `test the integrity of stored data for ProgressPage`() {
19 | val samplePageNumber = 1u
20 | val sampleListOfData: List =
21 | MutableList(Random.nextInt(from = 0, until = 100)) { index: Int ->
22 | return@MutableList "Num of index $index"
23 | }
24 | val progressPageState: PageState =
25 | ProgressPage(
26 | page = samplePageNumber,
27 | data = sampleListOfData.toList()
28 | )
29 | assertEquals(samplePageNumber, progressPageState.page)
30 | assertEquals(sampleListOfData, progressPageState.data)
31 | }
32 |
33 | @Test
34 | fun `test hashCode for ProgressPage`() {
35 | val page = 1u
36 | val progressPageState: PageState =
37 | ProgressPage(
38 | page = page,
39 | data = emptyList()
40 | )
41 | assertEquals(page.hashCode(), progressPageState.hashCode())
42 | }
43 |
44 | @Test
45 | fun `test compareTo for ProgressPage`() {
46 | val progressPageState1: PageState =
47 | ProgressPage(
48 | page = 1u,
49 | data = emptyList()
50 | )
51 | val progressPageState2: PageState =
52 | ProgressPage(
53 | page = 2u,
54 | data = emptyList()
55 | )
56 | assertTrue(progressPageState1 < progressPageState2)
57 | assertFalse(progressPageState1 > progressPageState2)
58 | }
59 |
60 | @Test
61 | fun `test copy for ProgressPage`() {
62 | val progressPageState: PageState =
63 | ProgressPage(
64 | page = 1u,
65 | data = listOf("a", "b", "c")
66 | )
67 |
68 | val copiedProgressPageState: PageState =
69 | progressPageState.copy()
70 |
71 | assertNotSame(progressPageState, copiedProgressPageState)
72 | assertEquals(progressPageState.page, copiedProgressPageState.page)
73 | assertEquals(progressPageState.data, copiedProgressPageState.data)
74 | assertEquals(progressPageState.hashCode(), copiedProgressPageState.hashCode())
75 | assertEquals(progressPageState.toString(), copiedProgressPageState.toString())
76 | assertFalse(progressPageState < copiedProgressPageState)
77 | assertFalse(progressPageState > copiedProgressPageState)
78 |
79 | val modifiedProgressPageState: PageState =
80 | progressPageState.copy(
81 | page = 2u,
82 | data = listOf("x", "y", "z")
83 | )
84 |
85 | assertNotSame(progressPageState, modifiedProgressPageState)
86 | assertNotEquals(progressPageState.page, modifiedProgressPageState.page)
87 | assertNotEquals(progressPageState.data, modifiedProgressPageState.data)
88 | assertNotEquals(progressPageState.hashCode(), modifiedProgressPageState.hashCode())
89 | assertNotEquals(progressPageState.toString(), modifiedProgressPageState.toString())
90 | assertTrue(progressPageState < modifiedProgressPageState)
91 | }
92 |
93 | @Test
94 | fun `test true using isProgressState for ProgressPage`() {
95 | val progressPageState: PageState =
96 | ProgressPage(
97 | page = 1u,
98 | data = emptyList()
99 | )
100 | assertTrue(progressPageState.isProgressState())
101 | }
102 |
103 | @Test
104 | fun `test using isRealProgressState for ProgressPage`() {
105 | val progressPageState: PageState =
106 | ProgressPage(
107 | page = 1u,
108 | data = emptyList()
109 | )
110 | assertTrue(progressPageState.isRealProgressState(ProgressPage::class))
111 | }
112 | }
--------------------------------------------------------------------------------
/paginator/src/test/java/com/jamal_aliev/paginator/pagestate/EmptyPageStateTest.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator.pagestate
2 |
3 | import com.jamal_aliev.paginator.extension.isEmptyState
4 | import com.jamal_aliev.paginator.extension.isRealEmptyState
5 | import com.jamal_aliev.paginator.page.PageState
6 | import com.jamal_aliev.paginator.page.PageState.EmptyPage
7 | import org.junit.Assert.assertEquals
8 | import org.junit.Assert.assertFalse
9 | import org.junit.Assert.assertNotEquals
10 | import org.junit.Assert.assertNotNull
11 | import org.junit.Assert.assertNotSame
12 | import org.junit.Assert.assertTrue
13 | import org.junit.Test
14 | import kotlin.random.Random
15 |
16 | class EmptyPageStateTest {
17 |
18 | @Test
19 | fun `test the integrity of stored data for EmptyPage`() {
20 | val samplePageNumber = 3u
21 | val sampleListOfData: List =
22 | MutableList(Random.nextInt(from = 0, until = 100)) { index: Int ->
23 | return@MutableList "Num of index $index"
24 | }
25 |
26 | val emptyPageState: EmptyPage =
27 | EmptyPage(
28 | page = samplePageNumber,
29 | data = sampleListOfData
30 | )
31 |
32 | assertEquals(samplePageNumber, emptyPageState.page)
33 | assertEquals(sampleListOfData, emptyPageState.data)
34 |
35 | assertNotNull(
36 | EmptyPage(
37 | page = 1u,
38 | data = emptyList()
39 | )
40 | )
41 | }
42 |
43 | @Test
44 | fun `test hashCode for EmptyPage`() {
45 | val page = 4u
46 | val emptyPageState: PageState =
47 | EmptyPage(
48 | page = page,
49 | data = emptyList()
50 | )
51 | assertEquals(page.hashCode(), emptyPageState.hashCode())
52 | }
53 |
54 | @Test
55 | fun `test compareTo for EmptyPage`() {
56 | val emptyPageState1: PageState =
57 | EmptyPage(
58 | page = 1u,
59 | data = emptyList()
60 | )
61 | val emptyPageState2: PageState =
62 | EmptyPage(
63 | page = 2u,
64 | data = emptyList()
65 | )
66 | assertTrue(emptyPageState1 < emptyPageState2)
67 | assertFalse(emptyPageState1 > emptyPageState2)
68 | }
69 |
70 | @Test
71 | fun `test copy for EmptyPage`() {
72 | val originalEmptyPageState: EmptyPage =
73 | EmptyPage(
74 | page = 1u,
75 | data = listOf("a", "b", "c")
76 | )
77 |
78 | val copiedEmptyPageState: EmptyPage =
79 | originalEmptyPageState.copy()
80 |
81 | assertNotSame(originalEmptyPageState, copiedEmptyPageState)
82 | assertEquals(originalEmptyPageState.page, copiedEmptyPageState.page)
83 | assertEquals(originalEmptyPageState.data, copiedEmptyPageState.data)
84 | assertEquals(originalEmptyPageState.hashCode(), copiedEmptyPageState.hashCode())
85 | assertEquals(originalEmptyPageState.toString(), copiedEmptyPageState.toString())
86 | assertFalse(originalEmptyPageState < copiedEmptyPageState)
87 | assertFalse(originalEmptyPageState > copiedEmptyPageState)
88 |
89 | val modifiedEmptyPageState: EmptyPage =
90 | originalEmptyPageState.copy(
91 | page = 3u,
92 | data = listOf("x", "y", "z")
93 | )
94 |
95 | assertNotSame(originalEmptyPageState, modifiedEmptyPageState)
96 | assertNotEquals(originalEmptyPageState.page, modifiedEmptyPageState.page)
97 | assertNotEquals(originalEmptyPageState.data, modifiedEmptyPageState.data)
98 | assertNotEquals(originalEmptyPageState.hashCode(), modifiedEmptyPageState.hashCode())
99 | assertNotEquals(originalEmptyPageState.toString(), modifiedEmptyPageState.toString())
100 | assertTrue(originalEmptyPageState < modifiedEmptyPageState)
101 | }
102 |
103 | @Test
104 | fun `test true using isEmptyState for EmptyPage`() {
105 | val emptyPageState: EmptyPage =
106 | EmptyPage(
107 | page = 1u,
108 | data = emptyList()
109 | )
110 | assertTrue(emptyPageState.isEmptyState())
111 | }
112 |
113 | @Test
114 | fun `test using isRealEmptyState for EmptyPage`() {
115 | val emptyPageState: EmptyPage =
116 | EmptyPage(
117 | page = 1u,
118 | data = emptyList()
119 | )
120 | assertTrue(emptyPageState.isRealEmptyState(EmptyPage::class))
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/paginator/src/test/java/com/jamal_aliev/paginator/pagestate/ErrorPageStateTest.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator.pagestate
2 |
3 | import com.jamal_aliev.paginator.extension.isErrorState
4 | import com.jamal_aliev.paginator.extension.isRealErrorState
5 | import com.jamal_aliev.paginator.page.PageState
6 | import com.jamal_aliev.paginator.page.PageState.ErrorPage
7 | import org.junit.Assert.assertEquals
8 | import org.junit.Assert.assertFalse
9 | import org.junit.Assert.assertNotEquals
10 | import org.junit.Assert.assertNotSame
11 | import org.junit.Assert.assertTrue
12 | import org.junit.Test
13 | import kotlin.random.Random
14 |
15 | class ErrorPageStateTest {
16 |
17 | @Test
18 | fun `test the integrity of stored data for ErrorPage`() {
19 | val sampleException = Exception("Test error")
20 | val samplePageNumber = 3u
21 | val sampleListOfData: List =
22 | MutableList(Random.nextInt(from = 0, until = 100)) { index: Int ->
23 | return@MutableList "Num of index $index"
24 | }
25 |
26 | val errorPageState: ErrorPage =
27 | ErrorPage(
28 | exception = sampleException,
29 | page = samplePageNumber,
30 | data = sampleListOfData
31 | )
32 |
33 | assertEquals(samplePageNumber, errorPageState.page)
34 | assertEquals(sampleListOfData, errorPageState.data)
35 | assertEquals(sampleException, errorPageState.exception)
36 | }
37 |
38 | @Test
39 | fun `test hashCode for ErrorPage`() {
40 | val page = 4u
41 | val errorPageState: PageState =
42 | ErrorPage(
43 | exception = Exception("Error"),
44 | page = page,
45 | data = emptyList()
46 | )
47 | assertEquals(page.hashCode(), errorPageState.hashCode())
48 | }
49 |
50 | @Test
51 | fun `test compareTo for ErrorPage`() {
52 | val errorPageState1: PageState =
53 | ErrorPage(
54 | exception = Exception("First error"),
55 | page = 1u,
56 | data = emptyList()
57 | )
58 | val errorPageState2: PageState =
59 | ErrorPage(
60 | exception = Exception("Second error"),
61 | page = 2u,
62 | data = emptyList()
63 | )
64 | assertTrue(errorPageState1 < errorPageState2)
65 | assertFalse(errorPageState1 > errorPageState2)
66 | }
67 |
68 | @Test
69 | fun `test copy for ErrorPage and preserving the exception`() {
70 | val originalErrorPageState: ErrorPage =
71 | ErrorPage(
72 | exception = Exception("Original error"),
73 | page = 1u,
74 | data = listOf("a", "b", "c")
75 | )
76 |
77 | val copiedErrorPageState: ErrorPage =
78 | originalErrorPageState.copy()
79 |
80 | assertNotSame(originalErrorPageState, copiedErrorPageState)
81 | assertEquals(originalErrorPageState.exception, copiedErrorPageState.exception)
82 | assertEquals(originalErrorPageState.page, copiedErrorPageState.page)
83 | assertEquals(originalErrorPageState.data, copiedErrorPageState.data)
84 | assertEquals(originalErrorPageState.hashCode(), copiedErrorPageState.hashCode())
85 | assertEquals(originalErrorPageState.toString(), copiedErrorPageState.toString())
86 | assertFalse(originalErrorPageState < copiedErrorPageState)
87 | assertFalse(originalErrorPageState > copiedErrorPageState)
88 |
89 | val modifiedErrorPageState: ErrorPage =
90 | originalErrorPageState.copy(
91 | exception = Exception("New error"),
92 | page = 3u,
93 | data = listOf("x", "y", "z")
94 | )
95 |
96 | assertNotSame(originalErrorPageState, modifiedErrorPageState)
97 | assertNotEquals(originalErrorPageState.exception, modifiedErrorPageState.exception)
98 | assertNotEquals(originalErrorPageState.page, modifiedErrorPageState.page)
99 | assertNotEquals(originalErrorPageState.data, modifiedErrorPageState.data)
100 | assertNotEquals(originalErrorPageState.hashCode(), modifiedErrorPageState.hashCode())
101 | assertNotEquals(originalErrorPageState.toString(), modifiedErrorPageState.toString())
102 | assertTrue(originalErrorPageState < modifiedErrorPageState)
103 | }
104 |
105 | @Test
106 | fun `test true using isErrorState for ErrorPage`() {
107 | val errorPageState: ErrorPage =
108 | ErrorPage(
109 | exception = Exception("Test error"),
110 | page = 1u,
111 | data = emptyList()
112 | )
113 | assertTrue(errorPageState.isErrorState())
114 | }
115 |
116 | @Test
117 | fun `test using isRealErrorState for ErrorPage`() {
118 | val errorPageState: ErrorPage =
119 | ErrorPage(
120 | exception = Exception("Test error"),
121 | page = 1u,
122 | data = emptyList()
123 | )
124 | assertTrue(errorPageState.isRealErrorState(ErrorPage::class))
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | xmlns:android
53 |
54 | ^$
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | xmlns:.*
64 |
65 | ^$
66 |
67 |
68 | BY_NAME
69 |
70 |
71 |
72 |
73 |
74 |
75 | .*:id
76 |
77 | http://schemas.android.com/apk/res/android
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | .*:name
87 |
88 | http://schemas.android.com/apk/res/android
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | name
98 |
99 | ^$
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | style
109 |
110 | ^$
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | .*
120 |
121 | ^$
122 |
123 |
124 | BY_NAME
125 |
126 |
127 |
128 |
129 |
130 |
131 | .*
132 |
133 | http://schemas.android.com/apk/res/android
134 |
135 |
136 | ANDROID_ATTRIBUTE_ORDER
137 |
138 |
139 |
140 |
141 |
142 |
143 | .*
144 |
145 | .*
146 |
147 |
148 | BY_NAME
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/paginator/src/test/java/com/jamal_aliev/paginator/pagestate/SuccessPageStateTest.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator.pagestate
2 |
3 | import com.jamal_aliev.paginator.extension.isEmptyState
4 | import com.jamal_aliev.paginator.extension.isRealSuccessState
5 | import com.jamal_aliev.paginator.extension.isSuccessState
6 | import com.jamal_aliev.paginator.page.PageState
7 | import com.jamal_aliev.paginator.page.PageState.SuccessPage
8 | import org.junit.Assert.assertEquals
9 | import org.junit.Assert.assertFalse
10 | import org.junit.Assert.assertNotEquals
11 | import org.junit.Assert.assertNotSame
12 | import org.junit.Assert.assertThrows
13 | import org.junit.Assert.assertTrue
14 | import org.junit.Test
15 | import kotlin.random.Random
16 |
17 | class SuccessPageStateTest {
18 |
19 | @Test
20 | fun `test the integrity of stored data for SuccessPage`() {
21 | val samplePageNumber = 3u
22 | val sampleListOfData: List =
23 | MutableList(Random.nextInt(from = 0, until = 100)) { index: Int ->
24 | return@MutableList "Num of index $index"
25 | }
26 |
27 | val successPageState: SuccessPage =
28 | SuccessPage(
29 | page = samplePageNumber,
30 | data = sampleListOfData
31 | )
32 |
33 | assertEquals(samplePageNumber, successPageState.page)
34 | assertEquals(sampleListOfData, successPageState.data)
35 |
36 | assertThrows(Exception::class.java) {
37 | SuccessPage(
38 | page = 1u,
39 | data = emptyList()
40 | )
41 | }
42 | }
43 |
44 | @Test
45 | fun `test hashCode for SuccessPage`() {
46 | val page = 4u
47 | val sampleListOfData: List =
48 | MutableList(Random.nextInt(from = 0, until = 100)) { index: Int ->
49 | return@MutableList "Num of index $index"
50 | }
51 | val successPageState: PageState =
52 | SuccessPage(
53 | page = page,
54 | data = sampleListOfData
55 | )
56 | assertEquals(page.hashCode(), successPageState.hashCode())
57 | }
58 |
59 | @Test
60 | fun `test compareTo for SuccessPage`() {
61 | val sampleListOfData: List =
62 | MutableList(Random.nextInt(from = 0, until = 100)) { index: Int ->
63 | return@MutableList "Num of index $index"
64 | }
65 | val successPageState1: PageState =
66 | SuccessPage(
67 | page = 1u,
68 | data = sampleListOfData
69 | )
70 | val successPageState2: PageState =
71 | SuccessPage(
72 | page = 2u,
73 | data = sampleListOfData
74 | )
75 | assertTrue(successPageState1 < successPageState2)
76 | assertFalse(successPageState1 > successPageState2)
77 | }
78 |
79 | @Test
80 | fun `test copy for SuccessPage`() {
81 | val originalSuccessPageState: SuccessPage =
82 | SuccessPage(
83 | page = 1u,
84 | data = listOf("a", "b", "c")
85 | )
86 |
87 | val copiedSuccessPageState: SuccessPage =
88 | originalSuccessPageState.copy()
89 |
90 | assertNotSame(originalSuccessPageState, copiedSuccessPageState)
91 | assertEquals(originalSuccessPageState.page, copiedSuccessPageState.page)
92 | assertEquals(originalSuccessPageState.data, copiedSuccessPageState.data)
93 | assertEquals(originalSuccessPageState.hashCode(), copiedSuccessPageState.hashCode())
94 | assertEquals(originalSuccessPageState.toString(), copiedSuccessPageState.toString())
95 | assertFalse(originalSuccessPageState < copiedSuccessPageState)
96 | assertFalse(originalSuccessPageState > copiedSuccessPageState)
97 |
98 | val modifiedSuccessPageState: SuccessPage =
99 | originalSuccessPageState.copy(
100 | page = 3u,
101 | data = listOf("x", "y", "z")
102 | )
103 |
104 | assertNotSame(originalSuccessPageState, modifiedSuccessPageState)
105 | assertNotEquals(originalSuccessPageState.page, modifiedSuccessPageState.page)
106 | assertNotEquals(originalSuccessPageState.data, modifiedSuccessPageState.data)
107 | assertNotEquals(originalSuccessPageState.hashCode(), modifiedSuccessPageState.hashCode())
108 | assertNotEquals(originalSuccessPageState.toString(), modifiedSuccessPageState.toString())
109 | assertTrue(originalSuccessPageState < modifiedSuccessPageState)
110 |
111 | val emptySuccessPageState:PageState =
112 | originalSuccessPageState.copy(
113 | data = emptyList()
114 | )
115 | assertNotSame(originalSuccessPageState, emptySuccessPageState)
116 | assertEquals(originalSuccessPageState.page, emptySuccessPageState.page)
117 | assertNotEquals(originalSuccessPageState.data, emptySuccessPageState.data)
118 | assertEquals(originalSuccessPageState.hashCode(), emptySuccessPageState.hashCode())
119 | assertNotEquals(originalSuccessPageState.toString(), emptySuccessPageState.toString())
120 | assertFalse(originalSuccessPageState < emptySuccessPageState)
121 | assertFalse(originalSuccessPageState > emptySuccessPageState)
122 | assertFalse(emptySuccessPageState.isSuccessState())
123 | assertTrue(emptySuccessPageState.isEmptyState())
124 | }
125 |
126 | @Test
127 | fun `test true using isSuccessState for SuccessPage`() {
128 | val sampleListOfData: List =
129 | MutableList(Random.nextInt(from = 0, until = 100)) { index: Int ->
130 | return@MutableList "Num of index $index"
131 | }
132 | val successPageState: SuccessPage =
133 | SuccessPage(
134 | page = 1u,
135 | data = sampleListOfData
136 | )
137 | assertTrue(successPageState.isSuccessState())
138 | }
139 |
140 | @Test
141 | fun `test using isRealSuccessState for SuccessPage`() {
142 | val sampleListOfData: List =
143 | MutableList(Random.nextInt(from = 0, until = 100)) { index: Int ->
144 | return@MutableList "Num of index $index"
145 | }
146 | val successPageState: SuccessPage =
147 | SuccessPage(
148 | page = 1u,
149 | data = sampleListOfData
150 | )
151 | assertTrue(successPageState.isRealSuccessState(SuccessPage::class))
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/paginator/src/main/java/com/jamal_aliev/paginator/extension/PageExt.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator.extension
2 |
3 | import com.jamal_aliev.paginator.page.PageState
4 | import com.jamal_aliev.paginator.page.PageState.EmptyPage
5 | import com.jamal_aliev.paginator.page.PageState.ErrorPage
6 | import com.jamal_aliev.paginator.page.PageState.ProgressPage
7 | import com.jamal_aliev.paginator.page.PageState.SuccessPage
8 | import kotlin.contracts.ExperimentalContracts
9 | import kotlin.contracts.contract
10 | import kotlin.reflect.KClass
11 |
12 | /**
13 | * Checks if the PageState is in progress state.
14 | *
15 | * @return True if the PageState is ProgressPage, false otherwise.
16 | */
17 | @OptIn(ExperimentalContracts::class)
18 | @Suppress("NOTHING_TO_INLINE")
19 | inline fun PageState?.isProgressState(): Boolean {
20 | contract {
21 | returns(true) implies (this@isProgressState is ProgressPage)
22 | }
23 | return this is ProgressPage<*>
24 | }
25 |
26 | /**
27 | * Checks if the PageState is a real progress state.
28 | *
29 | * @return True if the PageState is ProgressPage of type T, false otherwise.
30 | */
31 | @OptIn(ExperimentalContracts::class)
32 | @Suppress("NOTHING_TO_INLINE")
33 | inline fun PageState.isRealProgressState(
34 | clazz: KClass>
35 | ): Boolean {
36 | contract {
37 | returns(true) implies (this@isRealProgressState is ProgressPage)
38 | }
39 | return this.isProgressState() && clazz.isInstance(this)
40 | }
41 |
42 | /**
43 | * Checks if the PageState is in empty state.
44 | *
45 | * @return True if the PageState is EmptyPage, false otherwise.
46 | */
47 | @OptIn(ExperimentalContracts::class)
48 | @Suppress("NOTHING_TO_INLINE")
49 | inline fun PageState?.isEmptyState(): Boolean {
50 | contract {
51 | returns(true) implies (this@isEmptyState is EmptyPage)
52 | }
53 | return this is EmptyPage<*>
54 | }
55 |
56 | /**
57 | * Checks if the PageState is a real empty state.
58 | *
59 | * @return True if the PageState is EmptyPage of type T, false otherwise.
60 | */
61 | @OptIn(ExperimentalContracts::class)
62 | @Suppress("NOTHING_TO_INLINE")
63 | inline fun PageState.isRealEmptyState(
64 | clazz: KClass>
65 | ): Boolean {
66 | contract {
67 | returns(true) implies (this@isRealEmptyState is EmptyPage)
68 | }
69 | return this.isEmptyState() && clazz.isInstance(this)
70 | }
71 |
72 | /**
73 | * Checks if the PageState is in success state.
74 | *
75 | * @return True if the PageState is SuccessPage and not EmptyPage, false otherwise.
76 | */
77 | @OptIn(ExperimentalContracts::class)
78 | @Suppress("NOTHING_TO_INLINE")
79 | inline fun PageState?.isSuccessState(): Boolean {
80 | contract {
81 | returns(true) implies (this@isSuccessState is SuccessPage
82 | && this@isSuccessState !is EmptyPage)
83 | returns(true) implies (this@isSuccessState !is EmptyPage)
84 | }
85 | return this is SuccessPage<*> && this !is EmptyPage<*>
86 | }
87 |
88 | /**
89 | * Checks if the PageState is a real success state.
90 | *
91 | * @return True if the PageState is SuccessPage of type T, false otherwise.
92 | */
93 | @OptIn(ExperimentalContracts::class)
94 | @Suppress("NOTHING_TO_INLINE")
95 | inline fun PageState.isRealSuccessState(
96 | clazz: KClass>
97 | ): Boolean {
98 | contract {
99 | returns(true) implies (this@isRealSuccessState is SuccessPage
100 | && this@isRealSuccessState !is EmptyPage)
101 | returns(true) implies (this@isRealSuccessState !is EmptyPage)
102 | }
103 | return this.isSuccessState() && clazz.isInstance(this)
104 | }
105 |
106 | /**
107 | * Checks if the PageState is in error state.
108 | *
109 | * @return True if the PageState is ErrorPage, false otherwise.
110 | */
111 | @OptIn(ExperimentalContracts::class)
112 | @Suppress("NOTHING_TO_INLINE")
113 | inline fun PageState?.isErrorState(): Boolean {
114 | contract {
115 | returns(true) implies (this@isErrorState is ErrorPage)
116 | }
117 | return this is ErrorPage<*>
118 | }
119 |
120 | /**
121 | * Checks if the PageState is a real error state.
122 | *
123 | * @return True if the PageState is ErrorPage of type T, false otherwise.
124 | */
125 | @OptIn(ExperimentalContracts::class)
126 | @Suppress("NOTHING_TO_INLINE")
127 | inline fun PageState.isRealErrorState(
128 | clazz: KClass>
129 | ): Boolean {
130 | contract {
131 | returns(true) implies (this@isRealErrorState is ErrorPage)
132 | }
133 | return this.isErrorState() && clazz.isInstance(this)
134 | }
135 |
136 | /**
137 | * Determines whether this `PageState` is adjacent to, or identical with, another `PageState`
138 | * based on their positional gap.
139 | *
140 | * Two states are considered "near" if the unsigned distance between them
141 | * (as returned by `gap(other)`) is either `0u` or `1u`, meaning:
142 | *
143 | * - `0u` — both states refer to the same position;
144 | * - `1u` — the states are directly next to each other.
145 | *
146 | * This is a convenience infix function for expressing proximity between
147 | * two `PageState` instances in a clear and readable way.
148 | *
149 | * Note: If you want to know **exactly how far apart** two-page states are,
150 | * use [gap] instead of `near`.
151 | *
152 | * @param other The other `PageState` to compare with.
153 | * @return `true` if the gap is within `[0u, 1u]`, otherwise `false`.
154 | */
155 | @Suppress("NOTHING_TO_INLINE")
156 | inline infix fun PageState<*>.near(other: PageState<*>): Boolean {
157 | val gap: UInt = this gap other
158 | return gap == 0u || gap == 1u
159 | }
160 |
161 | /**
162 | * Determines whether this `PageState` is not adjacent to, and not identical with,
163 | * another `PageState` based on their positional gap.
164 | *
165 | * Two states are considered "far" if the unsigned distance between them
166 | * (as returned by `gap(other)`) is greater than `1u`, meaning:
167 | *
168 | * - the states do not represent the same position (`gap != 0u`), and
169 | * - they are not immediate neighbors (`gap != 1u`).
170 | *
171 | * This is the logical inverse of the `near` function and provides a readable
172 | * way to express non-proximity between `PageState` instances.
173 | *
174 | * Note: If you want to know **exactly how far apart** two-page states are,
175 | * use [gap] instead of `far`.
176 | *
177 | * @param other The other `PageState` to compare with.
178 | * @return `true` if the gap is outside the range `[0u, 1u]`, otherwise `false`.
179 | */
180 | @Suppress("NOTHING_TO_INLINE")
181 | inline infix fun PageState<*>.far(other: PageState<*>): Boolean {
182 | val gap: UInt = this gap other
183 | return gap != 0u && gap != 1u
184 | }
185 |
186 | /**
187 | * Computes the unsigned distance between this `PageState` and another `PageState`.
188 | *
189 | * The gap represents how far apart the two pages are, ignoring direction.
190 | * It is calculated as the difference between the larger and smaller page numbers:
191 | *
192 | * gap = if (this.page >= other.page) this.page - other.page else other.page - this.page
193 | *
194 | * Examples:
195 | * - States on the same page → `0u`
196 | * - Adjacent pages → `1u`
197 | * - Two pages apart → `2u`, and so on.
198 | *
199 | * This function is used by helpers like `near` and `far` to determine relative proximity
200 | * between page states.
201 | *
202 | * @param other The other `PageState` to measure the distance to.
203 | * @return A `UInt` representing the absolute page difference.
204 | */
205 | @Suppress("NOTHING_TO_INLINE")
206 | inline infix fun PageState<*>.gap(other: PageState<*>): UInt {
207 | return gapInternal(this.page, other.page)
208 | }
209 |
210 | @Suppress("NOTHING_TO_INLINE")
211 | internal inline infix fun PageState<*>.gap(other: UInt): UInt {
212 | return gapInternal(this.page, other)
213 | }
214 |
215 | @Suppress("NOTHING_TO_INLINE")
216 | internal inline infix fun UInt.gap(other: PageState<*>): UInt {
217 | return gapInternal(this, other.page)
218 | }
219 |
220 | @PublishedApi
221 | @Suppress("NOTHING_TO_INLINE")
222 | internal inline fun gapInternal(first: UInt, second: UInt): UInt {
223 | return if (first >= second) {
224 | first - second
225 | } else {
226 | second - first
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jamal_aliev/paginator/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.viewModels
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.layout.Box
9 | import androidx.compose.foundation.layout.Column
10 | import androidx.compose.foundation.layout.Spacer
11 | import androidx.compose.foundation.layout.fillMaxSize
12 | import androidx.compose.foundation.layout.fillMaxWidth
13 | import androidx.compose.foundation.layout.height
14 | import androidx.compose.foundation.lazy.LazyColumn
15 | import androidx.compose.foundation.lazy.rememberLazyListState
16 | import androidx.compose.material3.Button
17 | import androidx.compose.material3.CircularProgressIndicator
18 | import androidx.compose.material3.MaterialTheme
19 | import androidx.compose.material3.Surface
20 | import androidx.compose.material3.Text
21 | import androidx.compose.runtime.Composable
22 | import androidx.compose.runtime.LaunchedEffect
23 | import androidx.compose.runtime.derivedStateOf
24 | import androidx.compose.runtime.getValue
25 | import androidx.compose.runtime.remember
26 | import androidx.compose.ui.Alignment
27 | import androidx.compose.ui.Modifier
28 | import androidx.compose.ui.graphics.Color
29 | import androidx.compose.ui.unit.dp
30 | import androidx.compose.ui.unit.sp
31 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
32 | import com.jamal_aliev.paginator.MainViewState.DataState
33 | import com.jamal_aliev.paginator.extension.isEmptyState
34 | import com.jamal_aliev.paginator.extension.isErrorState
35 | import com.jamal_aliev.paginator.extension.isProgressState
36 | import com.jamal_aliev.paginator.extension.isSuccessState
37 | import com.jamal_aliev.paginator.page.PageState
38 | import com.jamal_aliev.paginator.ui.theme.PaginatorTheme
39 |
40 | // S E
41 | // snapshot: 1(s),2(s),3(s),4(e),5(e) ... 10(s)
42 | // goNext
43 |
44 | class MainActivity : ComponentActivity() {
45 |
46 | private val viewModel by viewModels { MainViewModel.Factory() }
47 |
48 | override fun onCreate(savedInstanceState: Bundle?) {
49 | super.onCreate(savedInstanceState)
50 |
51 | setContent {
52 | PaginatorTheme {
53 | Surface(
54 | modifier = Modifier.fillMaxSize(),
55 | color = MaterialTheme.colorScheme.background
56 | ) {
57 | val state by viewModel.state.collectAsStateWithLifecycle()
58 |
59 | when (state) {
60 | is DataState -> {
61 | DataState(state = state as DataState)
62 | }
63 |
64 | MainViewState.ProgressState -> {
65 | ProgressState()
66 | }
67 | }
68 | }
69 | }
70 | }
71 | }
72 |
73 | @Composable
74 | private fun ProgressState() {
75 | Box(contentAlignment = Alignment.Center) {
76 | CircularProgressIndicator()
77 | }
78 | }
79 |
80 | @Composable
81 | fun DataState(state: DataState) {
82 | val lazyListState = rememberLazyListState()
83 |
84 | val endOfListReached by remember {
85 | derivedStateOf {
86 | val currentIndex = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
87 | val totalCount = lazyListState.layoutInfo.totalItemsCount
88 | currentIndex != null && currentIndex >= totalCount - 10
89 | }
90 | }
91 | LaunchedEffect(endOfListReached) {
92 | if (endOfListReached) {
93 | viewModel.endReached()
94 | }
95 | }
96 |
97 | LazyColumn(state = lazyListState) {
98 | state.data.forEach { pageState : PageState ->
99 | when {
100 | pageState.isSuccessState() -> {
101 | items(pageState.data.size) { index ->
102 | Column(
103 | horizontalAlignment = Alignment.CenterHorizontally
104 | ) {
105 | if (index == 0) {
106 | Spacer(modifier = Modifier.height(16.dp))
107 | Text(text = "PageSuccess #${pageState.page}")
108 | }
109 |
110 | Box(
111 | modifier = Modifier
112 | .fillMaxWidth()
113 | .background(Color.Green),
114 | contentAlignment = Alignment.Center
115 | ) {
116 | StrItem(data = pageState.data[index])
117 | }
118 |
119 | if (index == pageState.data.lastIndex) {
120 | Text(text = "PageSuccess #${pageState.page}")
121 | Spacer(modifier = Modifier.height(16.dp))
122 | }
123 | }
124 | }
125 | }
126 |
127 | pageState.isEmptyState() -> {}
128 |
129 | pageState.isErrorState() -> {
130 | item {
131 | Column(
132 | modifier = Modifier
133 | .fillMaxWidth(),
134 | horizontalAlignment = Alignment.CenterHorizontally
135 | ) {
136 | Spacer(modifier = Modifier.height(16.dp))
137 | Text(text = "PageError #${pageState.page}")
138 | Column(
139 | modifier = Modifier
140 | .fillMaxWidth()
141 | .background(Color.Red),
142 | horizontalAlignment = Alignment.CenterHorizontally
143 | ) {
144 | Text(text = pageState.exception.message.toString())
145 | Button(onClick = { viewModel.refreshPage(pageState) }) {
146 | Text(text = "Refresh")
147 | }
148 | }
149 | Text(text = "PageError #${pageState.page}")
150 | Spacer(modifier = Modifier.height(16.dp))
151 | }
152 | }
153 | }
154 |
155 | pageState.isProgressState() -> {
156 | item {
157 | Column(
158 | modifier = Modifier
159 | .fillMaxWidth(),
160 | horizontalAlignment = Alignment.CenterHorizontally
161 | ) {
162 | Spacer(modifier = Modifier.height(16.dp))
163 | Text(text = "PageProgress #${pageState.page}")
164 |
165 | Box(
166 | modifier = Modifier
167 | .fillMaxWidth()
168 | .background(Color.Cyan),
169 | contentAlignment = Alignment.Center
170 | ) {
171 | CircularProgressIndicator()
172 | }
173 |
174 | Text(text = "PageProgress #${pageState.page}")
175 | Spacer(modifier = Modifier.height(16.dp))
176 | }
177 | }
178 | }
179 | }
180 | }
181 | }
182 | }
183 |
184 | @Composable
185 | fun StrItem(data: String) {
186 | Text(
187 | text = "Item $data",
188 | fontSize = 26.sp
189 | )
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/paginator/src/test/java/com/jamal_aliev/paginator/MutablePaginatorTest.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator
2 |
3 | import com.jamal_aliev.paginator.bookmark.Bookmark.BookmarkUInt
4 | import com.jamal_aliev.paginator.page.PageState
5 | import com.jamal_aliev.paginator.page.PageState.EmptyPage
6 | import com.jamal_aliev.paginator.page.PageState.ErrorPage
7 | import com.jamal_aliev.paginator.page.PageState.ProgressPage
8 | import com.jamal_aliev.paginator.page.PageState.SuccessPage
9 | import kotlinx.coroutines.delay
10 | import kotlinx.coroutines.runBlocking
11 | import org.junit.Assert.assertEquals
12 | import org.junit.Assert.assertFalse
13 | import org.junit.Assert.assertNull
14 | import org.junit.Assert.assertTrue
15 | import org.junit.Test
16 |
17 | class MutablePaginatorTest {
18 |
19 | @Test
20 | fun `test set get remove page state`() {
21 | val paginator = MutablePaginator { emptyList() }
22 | val pageStates: MutableList> =
23 | MutableList((1..10_000).random()) { index: Int ->
24 | createRandomPageState(page = index.toUInt(), listOf("$index page"))
25 | }
26 | pageStates.shuffled().forEach { pageState: PageState ->
27 | paginator.setState(pageState, silently = true)
28 | }
29 |
30 | assertEquals(pageStates.size, paginator.size)
31 |
32 | assertEquals(pageStates, paginator.pageStates)
33 | assertEquals(pageStates.map { it.page }, paginator.pages)
34 | pageStates.forEach { pageState: PageState ->
35 | assertEquals(pageState, paginator[pageState.page])
36 | }
37 | pageStates.forEach { pageState: PageState ->
38 | val removed = paginator.removeState(pageState.page, silently = true)!!
39 | assertEquals(pageState.page, removed.page)
40 | assertEquals(pageState.data, removed.data)
41 | }
42 |
43 | assertEquals(0, paginator.size)
44 | }
45 |
46 | @Test
47 | fun `test jump and next`(): Unit = runBlocking {
48 | val paginator = MutablePaginator { page: UInt ->
49 | Source.getByPage(page.toInt(), this.capacity)
50 | }
51 | do {
52 | paginator.jump(BookmarkUInt(page = 1u))
53 | } while (paginator[1u] !is SuccessPage<*>)
54 | assertEquals(1, paginator.size)
55 |
56 | do {
57 | paginator.goNextPage(silentlyLoading = true, silentlyResult = true)
58 | } while (paginator.size < 10)
59 | assertEquals(10, paginator.size)
60 | assertEquals(
61 | listOf(1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u),
62 | paginator.pages
63 | )
64 | }
65 |
66 | @Test
67 | fun `test jump and previous`(): Unit = runBlocking {
68 | val paginator = MutablePaginator { page: UInt ->
69 | Source.getByPage(page.toInt(), this.capacity)
70 | }
71 | do {
72 | paginator.jump(BookmarkUInt(page = 10u))
73 | } while (paginator[10u] !is SuccessPage<*>)
74 | assertEquals(1, paginator.size)
75 |
76 | do {
77 | paginator.goPreviousPage(silentlyLoading = true, silentlyResult = true)
78 | } while (paginator.size < 10)
79 | assertEquals(10, paginator.size)
80 | assertEquals(
81 | listOf(1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u),
82 | paginator.pages
83 | )
84 | }
85 |
86 | @Test
87 | fun `test jump next previous`(): Unit = runBlocking {
88 | val paginator = MutablePaginator { page: UInt ->
89 | Source.getByPage(page.toInt(), this.capacity)
90 | }
91 | do {
92 | paginator.jump(BookmarkUInt(page = 20u))
93 | } while (paginator[20u] !is SuccessPage<*>)
94 | assertEquals(1, paginator.size)
95 |
96 | do {
97 | paginator.goNextPage(silentlyLoading = true, silentlyResult = true)
98 | } while (paginator[40u] !is SuccessPage<*>)
99 | do {
100 | paginator.goPreviousPage(silentlyLoading = true, silentlyResult = true)
101 | } while (paginator[1u] !is SuccessPage<*>)
102 | assertEquals((1u..40u).toList(), paginator.pages)
103 | }
104 |
105 | @Test
106 | fun `test jump jump and remove`(): Unit = runBlocking {
107 | val paginator = MutablePaginator { emptyList() }.apply {
108 | resize(capacity = 1, resize = false, silently = true)
109 | }
110 | val data = listOf(
111 | SuccessPage(page = 1u, data = listOf("data of page")), // 0
112 | SuccessPage(page = 2u, data = listOf("data of page")), // 1
113 | SuccessPage(page = 3u, data = listOf("data of page")), // 2
114 | SuccessPage(page = 11u, data = listOf("data of page")), // 3
115 | SuccessPage(page = 12u, data = listOf("data of page")), // 4
116 | SuccessPage(page = 13u, data = listOf("data of page")), // 5
117 | SuccessPage(page = 21u, data = listOf("data of page")), // 6
118 | SuccessPage(page = 22u, data = listOf("data of page")), // 7
119 | SuccessPage(page = 23u, data = listOf("data of page")), // 8
120 | )
121 |
122 | assertFalse(paginator.isStarted)
123 | assertEquals(0, paginator.size)
124 | data.forEach(paginator::setState)
125 | assertEquals(data.size, paginator.size)
126 | paginator.jump(BookmarkUInt(page = 13u))
127 | assertTrue(paginator.isStarted)
128 | assertEquals(data[0], paginator[1u])
129 | assertEquals(data[1], paginator[2u])
130 | assertEquals(data[2], paginator[3u])
131 | assertEquals(data[3], paginator[11u])
132 | assertEquals(data[4], paginator[12u])
133 | assertEquals(data[5], paginator[13u])
134 | assertEquals(data[6], paginator[21u])
135 | assertEquals(data[7], paginator[22u])
136 | assertEquals(data[8], paginator[23u])
137 | assertEquals(11u, paginator.startContextPage)
138 | assertEquals(13u, paginator.endContextPage)
139 |
140 | assertEquals(data[1], paginator.removeState(pageToRemove = 2u))
141 | assertEquals(data[0], paginator[1u])
142 | assertEquals(data[2], paginator[2u])
143 | assertNull(paginator[3u])
144 | assertEquals(data[4], paginator[11u])
145 | assertEquals(data[5], paginator[12u])
146 | assertNull(paginator[13u])
147 | assertEquals(data[7], paginator[21u])
148 | assertEquals(data[8], paginator[22u])
149 | assertNull(paginator[23u])
150 | assertEquals(11u, paginator.startContextPage)
151 | assertEquals(12u, paginator.endContextPage)
152 |
153 | assertEquals(data[8], paginator.removeState(pageToRemove = 22u))
154 | assertEquals(data[0], paginator[1u])
155 | assertEquals(data[2], paginator[2u])
156 | assertNull(paginator[3u])
157 | assertEquals(data[4], paginator[11u])
158 | assertEquals(data[5], paginator[12u])
159 | assertNull(paginator[13u])
160 | assertEquals(data[7], paginator[21u])
161 | assertNull(paginator[22u])
162 | assertNull(paginator[23u])
163 | assertEquals(11u, paginator.startContextPage)
164 | assertEquals(12u, paginator.endContextPage)
165 |
166 | assertEquals(data[5], paginator.removeState(pageToRemove = 12u))
167 | assertEquals(data[0], paginator[1u])
168 | assertEquals(data[2], paginator[2u])
169 | assertNull(paginator[3u])
170 | assertEquals(data[4], paginator[11u])
171 | assertNull(paginator[12u])
172 | assertNull(paginator[13u])
173 | assertNull(paginator[21u])
174 | assertNull(paginator[22u])
175 | assertNull(paginator[23u])
176 | assertEquals(11u, paginator.startContextPage)
177 | assertEquals(11u, paginator.endContextPage)
178 |
179 | assertEquals(data[0], paginator.removeState(pageToRemove = 1u))
180 | assertEquals(data[2], paginator[1u])
181 | assertNull(paginator[2u])
182 | assertNull(paginator[3u])
183 | assertNull(paginator[11u])
184 | assertNull(paginator[12u])
185 | assertNull(paginator[13u])
186 | assertNull(paginator[21u])
187 | assertNull(paginator[22u])
188 | assertNull(paginator[23u])
189 | assertEquals(1u, paginator.startContextPage)
190 | assertEquals(1u, paginator.endContextPage)
191 |
192 | assertEquals(data[2], paginator.removeState(pageToRemove = 1u))
193 | assertNull(paginator[1u])
194 | assertNull(paginator[2u])
195 | assertNull(paginator[3u])
196 | assertNull(paginator[11u])
197 | assertNull(paginator[12u])
198 | assertNull(paginator[13u])
199 | assertNull(paginator[21u])
200 | assertNull(paginator[22u])
201 | assertNull(paginator[23u])
202 | assertEquals(0, paginator.size)
203 | assertEquals(0u, paginator.startContextPage)
204 | assertEquals(0u, paginator.endContextPage)
205 | }
206 |
207 |
208 | @Test
209 | fun `test context findNearContextPage`(): Unit = runBlocking {
210 | val paginator = MutablePaginator { emptyList() }.apply {
211 | resize(capacity = 1, resize = false, silently = true)
212 | }
213 | val data = listOf(
214 | SuccessPage(page = 1u, data = listOf("data of page")), // 0
215 | SuccessPage(page = 2u, data = listOf("data of page")), // 1
216 | SuccessPage(page = 3u, data = listOf("data of page")), // 2
217 | SuccessPage(page = 11u, data = listOf("data of page")), // 3
218 | SuccessPage(page = 12u, data = listOf("data of page")), // 4
219 | SuccessPage(page = 13u, data = listOf("data of page")), // 5
220 | SuccessPage(page = 21u, data = listOf("data of page")), // 6
221 | SuccessPage(page = 22u, data = listOf("data of page")), // 7
222 | SuccessPage(page = 23u, data = listOf("data of page")), // 8
223 | )
224 | assertFalse(paginator.isStarted)
225 | assertEquals(0, paginator.size)
226 | data.forEach(paginator::setState)
227 | assertEquals(data.size, paginator.size)
228 |
229 | paginator.findNearContextPage(startPoint = 1u)
230 | assertEquals(1u, paginator.startContextPage)
231 | assertEquals(3u, paginator.endContextPage)
232 |
233 | paginator.findNearContextPage(startPoint = 5u, endPoint = 12u)
234 | assertEquals(11u, paginator.startContextPage)
235 | assertEquals(13u, paginator.endContextPage)
236 |
237 | paginator.findNearContextPage(startPoint = 4u, endPoint = 6u)
238 | assertEquals(1u, paginator.startContextPage)
239 | assertEquals(3u, paginator.endContextPage)
240 |
241 | paginator.findNearContextPage(startPoint = 7u)
242 | assertEquals(1u, paginator.startContextPage)
243 | assertEquals(3u, paginator.endContextPage)
244 |
245 | paginator.findNearContextPage(startPoint = 7u, endPoint = 8u)
246 | assertEquals(11u, paginator.startContextPage)
247 | assertEquals(13u, paginator.endContextPage)
248 |
249 | paginator.findNearContextPage(startPoint = 9u, endPoint = 15u)
250 | assertEquals(11u, paginator.startContextPage)
251 | assertEquals(13u, paginator.endContextPage)
252 |
253 | paginator.findNearContextPage(startPoint = 9u, endPoint = 20u)
254 | assertEquals(21u, paginator.startContextPage)
255 | assertEquals(23u, paginator.endContextPage)
256 | }
257 | }
258 |
259 | private data object Source {
260 |
261 | private val data = MutableList(10_000) { "data of $it" }
262 |
263 | suspend fun getByPage(page: Int, size: Int): MutableList {
264 | delay((1L..10L).random())
265 | if ((0..100).random() < 25) throw Exception()
266 | require(page > 0)
267 | val startIndex = (page - 1) * size
268 | val endIndex = minOf(startIndex + size, data.size)
269 | return data.subList(startIndex, endIndex)
270 | }
271 | }
272 |
273 | private fun createRandomPageState(page: UInt, data: List): PageState {
274 | return when ((0..100).random()) {
275 | in 0..24 -> ProgressPage(page, data)
276 | in 25..49 -> EmptyPage(page, data)
277 | in 50..75 -> ErrorPage(Exception(), page, data)
278 | else -> SuccessPage(page, data)
279 | }
280 | }
281 |
--------------------------------------------------------------------------------
/paginator/src/main/java/com/jamal_aliev/paginator/extension/PaginatorExt.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator.extension
2 |
3 | import com.jamal_aliev.paginator.MutablePaginator
4 | import com.jamal_aliev.paginator.exception.LockedException.RefreshWasLockedException
5 | import com.jamal_aliev.paginator.initializer.InitializerEmptyPage
6 | import com.jamal_aliev.paginator.initializer.InitializerErrorPage
7 | import com.jamal_aliev.paginator.initializer.InitializerProgressPage
8 | import com.jamal_aliev.paginator.initializer.InitializerSuccessPage
9 | import com.jamal_aliev.paginator.page.PageState
10 |
11 | /**
12 | * Iterates through each PageState in the paginator and performs the given action on it.
13 | *
14 | * @param action The action to be performed on each PageState.
15 | */
16 | inline fun MutablePaginator.foreEach(
17 | action: (PageState) -> Unit
18 | ) {
19 | for (state in this) {
20 | action(state.value)
21 | }
22 | }
23 |
24 | /**
25 | * Iterates safely over all `PageState` items contained in this `MutablePaginator`,
26 | * allowing full control over how iteration starts, progresses, and stops.
27 | *
28 | * This function provides customizable strategies for:
29 | * - selecting the initial index,
30 | * - determining how the index changes on each step,
31 | * - defining the conditions under which iteration continues.
32 | *
33 | * This enables flexible traversal patterns such as forward or backward iteration,
34 | * skipping elements, conditional early termination, or implementing custom stepping logic.
35 | *
36 | * Iteration proceeds as long as:
37 | * - the index stays within the bounds of the state list, and
38 | * - `actionAndContinue` returns `true`.
39 | *
40 | * @param initialIndex A function that determines the starting index for iteration.
41 | * Defaults to `0` (beginning of the list).
42 | *
43 | * @param step A function that defines how to compute the next index value.
44 | * Defaults to incrementing by 1.
45 | *
46 | * @param actionAndContinue A callback invoked for each visited `PageState`.
47 | * Receives the full list of states, the current index, and the current state.
48 | * Returns `true` to continue iterating, or `false` to stop.
49 | *
50 | * @return The original list of page states (`pageStates`) after iteration completes.
51 | */
52 | inline fun MutablePaginator.smartForEach(
53 | initialIndex: (list: List>) -> Int = { 0 },
54 | step: (index: Int) -> Int = { it + 1 },
55 | actionAndContinue: (
56 | states: List>,
57 | index: Int,
58 | currentState: PageState
59 | ) -> Boolean
60 | ): List> {
61 | val states: List> = this.pageStates
62 | var index = initialIndex.invoke(states)
63 | while (0 <= index && index < states.size) {
64 | val currentState: PageState = states[index]
65 | if (!actionAndContinue.invoke(states, index, currentState)) {
66 | break
67 | }
68 | index = step.invoke(index)
69 | }
70 | return states
71 | }
72 |
73 | /**
74 | * Finds the index of the first element matching the given predicate in the paginator.
75 | *
76 | * @param predicate The predicate to match elements.
77 | * @return A pair containing the page number and index of the first matching element, or null if none found.
78 | */
79 | inline fun MutablePaginator.indexOfFirst(
80 | predicate: (T) -> Boolean
81 | ): Pair? {
82 | for (page in pageStates) {
83 | val result = page.data.indexOfFirst(predicate)
84 | if (result != -1) return page.page to result
85 | }
86 | return null
87 | }
88 |
89 | /**
90 | * Finds the index of the first element matching the given predicate in the specified page.
91 | *
92 | * @param page The page number to search in.
93 | * @param predicate The predicate to match elements.
94 | * @return A pair containing the page number and index of the first matching element, or null if none found.
95 | * @throws IllegalArgumentException if the page is not found.
96 | */
97 | inline fun MutablePaginator.indexOfFirst(
98 | page: UInt,
99 | predicate: (T) -> Boolean
100 | ): Pair? {
101 | val pageState = checkNotNull(getStateOf(page)) { "Page $page is not found" }
102 | for ((i, e) in pageState.data.withIndex()) {
103 | if (predicate(e)) {
104 | return page to i
105 | }
106 | }
107 | return null
108 | }
109 |
110 | /**
111 | * Finds the index of the last element matching the given predicate in the paginator.
112 | *
113 | * @param predicate The predicate to match elements.
114 | * @return A pair containing the page number and index of the last matching element, or null if none found.
115 | */
116 | inline fun MutablePaginator.indexOfLast(
117 | predicate: (T) -> Boolean
118 | ): Pair? {
119 | for (page in pageStates.reversed()) {
120 | val result = page.data.indexOfLast(predicate)
121 | if (result != -1) return page.page to result
122 | }
123 | return null
124 | }
125 |
126 | /**
127 | * Finds the index of the last element matching the given predicate in the specified page.
128 | *
129 | * @param page The page number to search in.
130 | * @param predicate The predicate to match elements.
131 | * @return A pair containing the page number and index of the last matching element, or null if none found.
132 | * @throws IllegalArgumentException if the page is not found.
133 | */
134 | inline fun MutablePaginator.indexOfLast(
135 | page: UInt,
136 | predicate: (T) -> Boolean
137 | ): Pair? {
138 | val pageState = checkNotNull(getStateOf(page)) { "Page $page is not found" }
139 | for ((index, element) in pageState.data.reversed().withIndex()) {
140 | if (predicate(element)) {
141 | return page to index
142 | }
143 | }
144 | return null
145 | }
146 |
147 | /**
148 | * Walks forward from the given [pivotState] through consecutive pages
149 | * that satisfy the [predicate], and returns the last page in that chain.
150 | *
151 | * This function:
152 | * - Starts at [pivotState].
153 | * - Moves to the next page (`page + 1`) as long as the page exists in the cache
154 | * and satisfies the [predicate].
155 | * - Stops at the last consecutive page that satisfies the predicate.
156 | *
157 | * @param pivotState The initial page from which forward traversal begins.
158 | * If null or does not satisfy [predicate], the function returns null.
159 | *
160 | * @param predicate A condition that each traversed page must satisfy.
161 | * Defaults to always true, allowing traversal through all consecutive next pages.
162 | *
163 | * @return The last PageState encountered while moving forward that still satisfies [predicate],
164 | * or null if the starting page is null or fails the predicate.
165 | */
166 | inline fun MutablePaginator.walkForwardWhile(
167 | pivotState: PageState?,
168 | predicate: (PageState) -> Boolean = { true }
169 | ): PageState? {
170 | return walkWhile(
171 | pivotState = pivotState,
172 | next = { currentPage: UInt ->
173 | return@walkWhile currentPage + 1u
174 | },
175 | predicate = { state: PageState ->
176 | return@walkWhile predicate.invoke(state)
177 | }
178 | )
179 | }
180 |
181 | /**
182 | * Walks backward from the given [pivotState], following consecutive previous pages,
183 | * and returns the first page in that backward chain that does *not* satisfy the [predicate].
184 | *
185 | * In other words, this function:
186 | * - Starts at [pivotState].
187 | * - Moves to the previous page (`page - 1`) as long as:
188 | * - The page exists in the cache, and
189 | * - The page satisfies the [predicate].
190 | * - Stops at the last page that satisfied the predicate.
191 | *
192 | * This effectively finds the earliest consecutive page before [pivotState]
193 | * that still matches [predicate].
194 | *
195 | * @param pivotState The initial page from which backward traversal begins.
196 | * If null or does not satisfy [predicate], the function returns null.
197 | *
198 | * @param predicate A condition that each traversed page must satisfy.
199 | * Defaults to always true, allowing traversal through all consecutive previous pages.
200 | *
201 | * @return The last PageState encountered while moving backward that still satisfies [predicate],
202 | * or null if the starting page is null or fails the predicate.
203 | */
204 | inline fun MutablePaginator.walkBackwardWhile(
205 | pivotState: PageState?,
206 | predicate: (PageState) -> Boolean = { true }
207 | ): PageState? {
208 | return walkWhile(
209 | pivotState = pivotState,
210 | next = { currentPage: UInt ->
211 | return@walkWhile currentPage - 1u
212 | },
213 | predicate = { state: PageState ->
214 | return@walkWhile predicate.invoke(state)
215 | }
216 | )
217 | }
218 |
219 | fun MutablePaginator.removeElement(predicate: (T) -> Boolean): T? {
220 | for (page in pages) {
221 | val removed: T? = removeElement(page, predicate)
222 | if (removed != null) {
223 | return removed
224 | }
225 | }
226 | return null
227 | }
228 |
229 | fun MutablePaginator.removeElement(page: UInt, predicate: (T) -> Boolean): T? {
230 | val state: PageState? = getStateOf(page)
231 | state ?: return null
232 | for ((index, element) in state.data.withIndex()) {
233 | if (predicate(element)) {
234 | return removeElement(
235 | page = page,
236 | index = index
237 | )
238 | }
239 | }
240 | return null
241 | }
242 |
243 | fun MutablePaginator.addElement(
244 | element: T,
245 | silently: Boolean = false,
246 | initSuccessPageState: ((page: UInt, data: List) -> PageState)? = null
247 | ): Boolean {
248 | val lastPage: UInt = pages.lastOrNull() ?: return false
249 | val lastIndex: Int = getStateOf(lastPage)?.data?.lastIndex ?: return false
250 | addElement(element, lastPage, lastIndex, silently, initSuccessPageState)
251 | return true
252 | }
253 |
254 | fun MutablePaginator.addElement(
255 | element: T,
256 | page: UInt,
257 | index: Int,
258 | silently: Boolean = false,
259 | initPageState: ((page: UInt, data: List) -> PageState)? = null
260 | ) {
261 | return addAllElements(
262 | elements = listOf(element),
263 | targetPage = page,
264 | index = index,
265 | silently = silently,
266 | initPageState = initPageState
267 | )
268 | }
269 |
270 | inline fun MutablePaginator.getElement(
271 | predicate: (T) -> Boolean
272 | ): T? {
273 | this.smartForEach { _, _, pageState: PageState ->
274 | for (element in pageState.data) {
275 | if (predicate(element)) {
276 | return element
277 | }
278 | }
279 | return@smartForEach true
280 | }
281 | return null
282 | }
283 |
284 | inline fun MutablePaginator.setElement(
285 | element: T,
286 | silently: Boolean = false,
287 | predicate: (T) -> Boolean
288 | ) {
289 | this.smartForEach { _, _, pageState ->
290 | var index = 0
291 | while (index < pageState.data.size) {
292 | if (predicate(pageState.data[index++])) {
293 | setElement(
294 | element = element,
295 | page = pageState.page,
296 | index = index,
297 | silently = silently
298 | )
299 | return
300 | }
301 | }
302 | return@smartForEach true
303 | }
304 | }
305 |
306 | /**
307 | * Refreshes all pages in the cache by setting their state to a progress state, loading their data,
308 | * and then updating their state based on the loaded data.
309 | *
310 | * @param loadingSilently If true, the loading state will not trigger snapshot updates.
311 | * @param finalSilently If true, the final state will not trigger snapshot updates.
312 | * @param initProgressState Initializer for the progress state.
313 | * @param initEmptyState Initializer for the empty state.
314 | * @param initSuccessState Initializer for the success state.
315 | * @param initErrorState Initializer for the error state.
316 | * @throws RefreshWasLockedException if the refresh operation is locked.
317 | */
318 | suspend fun MutablePaginator.refreshAll(
319 | loadingSilently: Boolean = false,
320 | finalSilently: Boolean = false,
321 | enableCacheFlow: Boolean = this.enableCacheFlow,
322 | initProgressState: InitializerProgressPage = this.initializerProgressPage,
323 | initEmptyState: InitializerEmptyPage = this.initializerEmptyPage,
324 | initSuccessState: InitializerSuccessPage = this.initializerSuccessPage,
325 | initErrorState: InitializerErrorPage = this.initializerErrorPage
326 | ) {
327 | if (lockRefresh) throw RefreshWasLockedException()
328 | return refresh(
329 | pages = this.pages,
330 | loadingSilently = loadingSilently,
331 | finalSilently = finalSilently,
332 | enableCacheFlow = enableCacheFlow,
333 | initProgressState = initProgressState,
334 | initEmptyState = initEmptyState,
335 | initSuccessState = initSuccessState,
336 | initErrorState = initErrorState
337 | )
338 | }
339 |
340 |
341 |
--------------------------------------------------------------------------------
/paginator/src/main/java/com/jamal_aliev/paginator/MutablePaginator.kt:
--------------------------------------------------------------------------------
1 | package com.jamal_aliev.paginator
2 |
3 | import com.jamal_aliev.paginator.bookmark.Bookmark
4 | import com.jamal_aliev.paginator.bookmark.Bookmark.BookmarkUInt
5 | import com.jamal_aliev.paginator.exception.LockedException.GoNextPageWasLockedException
6 | import com.jamal_aliev.paginator.exception.LockedException.GoPreviousPageWasLockedException
7 | import com.jamal_aliev.paginator.exception.LockedException.JumpWasLockedException
8 | import com.jamal_aliev.paginator.exception.LockedException.RefreshWasLockedException
9 | import com.jamal_aliev.paginator.exception.LockedException.RestartWasLockedException
10 | import com.jamal_aliev.paginator.extension.far
11 | import com.jamal_aliev.paginator.extension.gap
12 | import com.jamal_aliev.paginator.extension.isProgressState
13 | import com.jamal_aliev.paginator.extension.isSuccessState
14 | import com.jamal_aliev.paginator.extension.smartForEach
15 | import com.jamal_aliev.paginator.extension.walkBackwardWhile
16 | import com.jamal_aliev.paginator.extension.walkForwardWhile
17 | import com.jamal_aliev.paginator.initializer.InitializerEmptyPage
18 | import com.jamal_aliev.paginator.initializer.InitializerErrorPage
19 | import com.jamal_aliev.paginator.initializer.InitializerProgressPage
20 | import com.jamal_aliev.paginator.initializer.InitializerSuccessPage
21 | import com.jamal_aliev.paginator.page.PageState
22 | import com.jamal_aliev.paginator.page.PageState.EmptyPage
23 | import com.jamal_aliev.paginator.page.PageState.ErrorPage
24 | import com.jamal_aliev.paginator.page.PageState.ProgressPage
25 | import com.jamal_aliev.paginator.page.PageState.SuccessPage
26 | import kotlinx.coroutines.async
27 | import kotlinx.coroutines.coroutineScope
28 | import kotlinx.coroutines.flow.Flow
29 | import kotlinx.coroutines.flow.MutableStateFlow
30 | import kotlinx.coroutines.flow.asStateFlow
31 | import kotlinx.coroutines.flow.filter
32 | import kotlinx.coroutines.flow.map
33 | import kotlinx.coroutines.flow.update
34 | import kotlin.contracts.ExperimentalContracts
35 | import kotlin.contracts.contract
36 | import kotlin.math.max
37 |
38 | open class MutablePaginator(
39 | var source: suspend MutablePaginator.(page: UInt) -> List
40 | ) : Comparable> {
41 |
42 | var capacity: Int = DEFAULT_CAPACITY
43 | private set
44 |
45 | val isCapacityUnlimited: Boolean
46 | get() = capacity == UNLIMITED_CAPACITY
47 |
48 | protected val cache = sortedMapOf>()
49 | val pages: List get() = cache.keys.toList()
50 | val pageStates: List> get() = cache.values.toList()
51 | val size: Int get() = cache.size
52 |
53 | var enableCacheFlow = false
54 | private set
55 | private val _cacheFlow = MutableStateFlow(false to cache)
56 | fun asFlow(): Flow