├── sample
├── gradlew
├── ios-app
│ ├── src
│ │ ├── Assets.xcassets
│ │ │ ├── Contents.json
│ │ │ └── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ ├── AppDelegate.swift
│ │ ├── Info.plist
│ │ ├── Resources
│ │ │ ├── ProductTableViewCell.xib
│ │ │ ├── Base.lproj
│ │ │ │ ├── LaunchScreen.storyboard
│ │ │ │ └── Main.storyboard
│ │ │ └── LoadingTableViewCell.xib
│ │ └── TestViewController.swift
│ ├── TestProj.xcodeproj
│ │ ├── project.xcworkspace
│ │ │ └── contents.xcworkspacedata
│ │ └── project.pbxproj
│ ├── TestProj.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── Podfile
│ └── Podfile.lock
├── mpp-library
│ ├── src
│ │ ├── androidMain
│ │ │ └── AndroidManifest.xml
│ │ └── commonMain
│ │ │ └── kotlin
│ │ │ └── com
│ │ │ └── icerockdev
│ │ │ └── library
│ │ │ └── ListViewModel.kt
│ ├── build.gradle.kts
│ └── MultiPlatformLibrary.podspec
└── android-app
│ ├── src
│ └── main
│ │ ├── res
│ │ └── layout
│ │ │ ├── unit_loading.xml
│ │ │ ├── unit_product.xml
│ │ │ └── activity_main.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── icerockdev
│ │ ├── LoadingUnitItem.kt
│ │ └── MainActivity.kt
│ ├── proguard-rules.pro
│ └── build.gradle.kts
├── img
└── logo.png
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── paging
├── src
│ ├── androidMain
│ │ └── AndroidManifest.xml
│ ├── iosTest
│ │ └── kotlin
│ │ │ └── dev
│ │ │ └── icerock
│ │ │ └── moko
│ │ │ └── paging
│ │ │ ├── BaseTestsClass.kt
│ │ │ └── Utils.kt
│ ├── jvmTest
│ │ └── kotlin
│ │ │ └── dev
│ │ │ └── icerock
│ │ │ └── moko
│ │ │ └── paging
│ │ │ ├── BaseTestsClass.kt
│ │ │ └── runTest.kt
│ ├── commonTest
│ │ └── kotlin
│ │ │ └── dev
│ │ │ └── icerock
│ │ │ └── moko
│ │ │ └── paging
│ │ │ ├── BaseTestsClass.kt
│ │ │ ├── Utils.kt
│ │ │ ├── TestListDataSource.kt
│ │ │ ├── PaginationTest.kt
│ │ │ └── IntegrationTests.kt
│ ├── iosMain
│ │ └── kotlin
│ │ │ └── Dummy.kt
│ ├── commonMain
│ │ └── kotlin
│ │ │ └── dev
│ │ │ └── icerock
│ │ │ └── moko
│ │ │ └── paging
│ │ │ ├── PagedListDataSource.kt
│ │ │ ├── LambdaPagedListDataSource.kt
│ │ │ ├── LiveDataExt.kt
│ │ │ ├── ReachEndNotifierList.kt
│ │ │ └── Pagination.kt
│ └── androidTest
│ │ └── kotlin
│ │ └── dev
│ │ └── icerock
│ │ └── moko
│ │ └── paging
│ │ ├── UtilsJvm.kt
│ │ └── BaseTestsClass.kt
└── build.gradle.kts
├── .gitignore
├── settings.gradle.kts
├── gradle.properties
├── .github
└── workflows
│ ├── publish.yml
│ └── compilation-check.yml
├── CONTRIBUTING.md
├── gradlew.bat
├── README.md
├── gradlew
└── LICENSE.md
/sample/gradlew:
--------------------------------------------------------------------------------
1 | ../gradlew
--------------------------------------------------------------------------------
/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oianmol/moko-paging-js/master/img/logo.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oianmol/moko-paging-js/master/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/paging/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sample/ios-app/src/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/sample/mpp-library/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | .settings
3 | .project
4 | .classpath
5 | .vscode
6 | .idea
7 | build
8 | *.iml
9 | Pods
10 | xcuserdata
11 | local.properties
12 | local.gradle
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/sample/ios-app/TestProj.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/paging/src/iosTest/kotlin/dev/icerock/moko/paging/BaseTestsClass.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.paging
6 |
7 | actual open class BaseTestsClass
8 |
--------------------------------------------------------------------------------
/paging/src/jvmTest/kotlin/dev/icerock/moko/paging/BaseTestsClass.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.paging
6 |
7 | actual open class BaseTestsClass
8 |
--------------------------------------------------------------------------------
/paging/src/commonTest/kotlin/dev/icerock/moko/paging/BaseTestsClass.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.paging
6 |
7 | expect open class BaseTestsClass()
8 |
--------------------------------------------------------------------------------
/paging/src/jvmTest/kotlin/dev/icerock/moko/paging/runTest.kt:
--------------------------------------------------------------------------------
1 | package dev.icerock.moko.paging
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 |
5 | actual fun runTest(block: suspend CoroutineScope.() -> T): T =
6 | kotlinx.coroutines.runBlocking(block = block)
7 |
--------------------------------------------------------------------------------
/paging/src/iosMain/kotlin/Dummy.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.paging
6 |
7 | // required for produce `metadata/iosMain`
8 | internal val sDummyVar: Int? = null
9 |
--------------------------------------------------------------------------------
/sample/ios-app/TestProj.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/sample/ios-app/TestProj.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/paging/src/commonMain/kotlin/dev/icerock/moko/paging/PagedListDataSource.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.paging
6 |
7 | interface PagedListDataSource {
8 | suspend fun loadPage(currentList: List?): List
9 | }
10 |
--------------------------------------------------------------------------------
/paging/src/androidTest/kotlin/dev/icerock/moko/paging/UtilsJvm.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.paging
6 |
7 | import kotlinx.coroutines.CoroutineScope
8 |
9 | actual fun runTest(block: suspend CoroutineScope.() -> T): T =
10 | kotlinx.coroutines.runBlocking(block = block)
11 |
--------------------------------------------------------------------------------
/paging/src/androidTest/kotlin/dev/icerock/moko/paging/BaseTestsClass.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.paging
6 |
7 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
8 | import org.junit.Rule
9 |
10 | actual open class BaseTestsClass {
11 | @get:Rule
12 | val rule = InstantTaskExecutorRule()
13 | }
14 |
--------------------------------------------------------------------------------
/sample/android-app/src/main/res/layout/unit_loading.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/paging/src/commonMain/kotlin/dev/icerock/moko/paging/LambdaPagedListDataSource.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.paging
6 |
7 | class LambdaPagedListDataSource(
8 | private val loadPageLambda: suspend (List?) -> List
9 | ) : PagedListDataSource {
10 | override suspend fun loadPage(currentList: List?): List {
11 | return loadPageLambda(currentList)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/sample/ios-app/src/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | import UIKit
6 |
7 | @UIApplicationMain
8 | class AppDelegate: NSObject, UIApplicationDelegate {
9 |
10 | var window: UIWindow?
11 |
12 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
13 |
14 | return true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/paging/src/commonTest/kotlin/dev/icerock/moko/paging/Utils.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.paging
6 |
7 | import kotlinx.coroutines.CoroutineScope
8 |
9 | expect fun runTest(block: suspend CoroutineScope.() -> T): T
10 |
11 | fun List.compareWith(list: List): Boolean {
12 | if(size != list.size) return false
13 |
14 | return zip(list).all { (item1, item2) ->
15 | item1 == item2
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/paging/src/commonTest/kotlin/dev/icerock/moko/paging/TestListDataSource.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.paging
6 |
7 | class TestListDataSource(val pageSize: Int, val totalPagesCount: Int) : PagedListDataSource {
8 | val dataList = (0 .. pageSize * totalPagesCount).map { it }
9 |
10 | override suspend fun loadPage(currentList: List?): List {
11 | val offset = currentList?.size ?: 0
12 | return dataList.subList(offset, offset + pageSize)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/sample/ios-app/Podfile:
--------------------------------------------------------------------------------
1 | source 'https://cdn.cocoapods.org/'
2 |
3 | # ignore all warnings from all pods
4 | inhibit_all_warnings!
5 |
6 | use_frameworks!
7 | platform :ios, '11.0'
8 |
9 | # workaround for https://github.com/CocoaPods/CocoaPods/issues/8073
10 | # need for correct invalidate of cache MultiPlatformLibrary.framework
11 | install! 'cocoapods', :disable_input_output_paths => true
12 |
13 | target 'TestProj' do
14 | # MultiPlatformLibrary
15 | pod 'MultiPlatformLibrary', :path => '../mpp-library'
16 |
17 | pod 'MultiPlatformLibraryUnits', :git => 'https://github.com/icerockdev/moko-units.git', :tag => 'release/0.4.0'
18 | end
19 |
--------------------------------------------------------------------------------
/sample/android-app/src/main/res/layout/unit_product.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
15 |
16 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/sample/android-app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /opt/android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | plugins {
6 | id("com.gradle.enterprise") version "3.10.1"
7 | }
8 |
9 | enableFeaturePreview("VERSION_CATALOGS")
10 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
11 |
12 | dependencyResolutionManagement {
13 | repositories {
14 | mavenCentral()
15 | google()
16 | }
17 | }
18 |
19 | gradleEnterprise {
20 | buildScan {
21 | publishAlwaysIf(System.getenv("CI").isNullOrEmpty().not())
22 | termsOfServiceUrl = "https://gradle.com/terms-of-service"
23 | termsOfServiceAgree = "yes"
24 | }
25 | }
26 |
27 | include(":paging")
28 | include(":sample:android-app")
29 | include(":sample:mpp-library")
30 |
--------------------------------------------------------------------------------
/sample/mpp-library/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | plugins {
6 | id("dev.icerock.moko.gradle.multiplatform.mobile")
7 | id("dev.icerock.mobile.multiplatform.ios-framework")
8 | id("dev.icerock.moko.gradle.detekt")
9 | }
10 |
11 | dependencies {
12 | commonMainImplementation(libs.coroutines)
13 |
14 | commonMainApi(projects.paging)
15 | commonMainApi(libs.mokoUnits)
16 | commonMainApi(libs.mokoMvvmLiveData)
17 | commonMainApi(libs.mokoMvvmState)
18 | commonMainApi(libs.mokoResources)
19 |
20 | androidMainImplementation(libs.lifecycle)
21 | }
22 |
23 | framework {
24 | export(libs.mokoUnits)
25 | export(libs.mokoMvvmLiveData)
26 | export(libs.mokoMvvmState)
27 | }
28 |
--------------------------------------------------------------------------------
/paging/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | plugins {
6 | id("dev.icerock.moko.gradle.multiplatform.mobile")
7 | id("dev.icerock.moko.gradle.publication")
8 | id("dev.icerock.moko.gradle.stub.javadoc")
9 | id("dev.icerock.moko.gradle.detekt")
10 | }
11 |
12 | kotlin {
13 | jvm()
14 | }
15 |
16 | dependencies {
17 | commonMainImplementation(libs.coroutines)
18 | commonMainApi(libs.mokoMvvmLiveData)
19 | commonMainApi(libs.mokoMvvmState)
20 |
21 | commonTestImplementation(libs.kotlinTestJUnit)
22 | androidTestImplementation(libs.androidCoreTesting)
23 | commonTestImplementation(libs.ktorClient)
24 | commonTestImplementation(libs.ktorClientMock)
25 | iosX64TestImplementation(libs.coroutines)
26 | }
27 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4096m
2 | org.gradle.configureondemand=false
3 | org.gradle.parallel=true
4 |
5 | kotlin.code.style=official
6 | kotlin.native.enableDependencyPropagation=false
7 | kotlin.mpp.enableGranularSourceSetsMetadata=true
8 | kotlin.mpp.enableCompatibilityMetadataVariant=true
9 |
10 | android.useAndroidX=true
11 |
12 | moko.android.targetSdk=31
13 | moko.android.compileSdk=31
14 | moko.android.minSdk=16
15 |
16 | moko.publish.name=MOKO paging
17 | moko.publish.description=Pagination logic in common code for mobile (android & ios) Kotlin Multiplatform development
18 | moko.publish.repo.org=icerockdev
19 | moko.publish.repo.name=moko-paging
20 | moko.publish.license=Apache-2.0
21 | moko.publish.developers=alex009|Aleksey Mikhailov|Aleksey.Mikhailov@icerockdev.com,Tetraquark|Vladislav Areshkin|vareshkin@icerockdev.com
22 |
--------------------------------------------------------------------------------
/paging/src/commonMain/kotlin/dev/icerock/moko/paging/LiveDataExt.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.paging
6 |
7 | import dev.icerock.moko.mvvm.livedata.LiveData
8 | import dev.icerock.moko.mvvm.livedata.map
9 | import dev.icerock.moko.mvvm.livedata.mediatorOf
10 |
11 | fun LiveData>.withLoadingItem(
12 | loading: LiveData,
13 | itemFactory: () -> T
14 | ): LiveData> = mediatorOf(this, loading) { items, nextPageLoading ->
15 | if (nextPageLoading) {
16 | items + itemFactory()
17 | } else {
18 | items
19 | }
20 | }
21 |
22 | fun LiveData>.withReachEndNotifier(
23 | action: (Int) -> Unit
24 | ): LiveData> = map { list ->
25 | list.withReachEndNotifier(action)
26 | }
27 |
--------------------------------------------------------------------------------
/sample/android-app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/sample/android-app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
12 |
13 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/sample/ios-app/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - MultiPlatformLibrary (0.1.0)
3 | - MultiPlatformLibraryUnits (0.4.0):
4 | - MultiPlatformLibrary
5 | - MultiPlatformLibraryUnits/Core (= 0.4.0)
6 | - MultiPlatformLibraryUnits/Core (0.4.0):
7 | - MultiPlatformLibrary
8 |
9 | DEPENDENCIES:
10 | - MultiPlatformLibrary (from `../mpp-library`)
11 | - MultiPlatformLibraryUnits (from `https://github.com/icerockdev/moko-units.git`, tag `release/0.4.0`)
12 |
13 | EXTERNAL SOURCES:
14 | MultiPlatformLibrary:
15 | :path: "../mpp-library"
16 | MultiPlatformLibraryUnits:
17 | :git: https://github.com/icerockdev/moko-units.git
18 | :tag: release/0.4.0
19 |
20 | CHECKOUT OPTIONS:
21 | MultiPlatformLibraryUnits:
22 | :git: https://github.com/icerockdev/moko-units.git
23 | :tag: release/0.4.0
24 |
25 | SPEC CHECKSUMS:
26 | MultiPlatformLibrary: 2a9f43df7bd018c32611a2087c1e2ef74847394c
27 | MultiPlatformLibraryUnits: 0d2efb66522f63a8b38d5639d329aeb70cd4e9ba
28 |
29 | PODFILE CHECKSUM: ef522d3d1655c544774fcadd6504f1f42278f721
30 |
31 | COCOAPODS: 1.11.2
32 |
--------------------------------------------------------------------------------
/sample/android-app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | plugins {
6 | id("dev.icerock.moko.gradle.android.application")
7 | id("dev.icerock.mobile.multiplatform-units")
8 | id("kotlin-kapt")
9 | }
10 |
11 | android {
12 | buildFeatures.dataBinding = true
13 |
14 | defaultConfig {
15 | applicationId = "dev.icerock.moko.samples.paging"
16 |
17 | versionCode = 1
18 | versionName = "0.1.0"
19 |
20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
21 | }
22 | }
23 |
24 | dependencies {
25 | implementation(libs.appCompat)
26 | implementation(libs.recyclerView)
27 | implementation(libs.lifecycle)
28 | implementation(libs.swipeRefreshLayout)
29 | implementation(libs.mokoUnitsDataBinding)
30 |
31 | implementation(projects.sample.mppLibrary)
32 | }
33 |
34 | multiplatformUnits {
35 | classesPackage = "com.icerockdev"
36 | dataBindingPackage = "com.icerockdev"
37 | layoutsSourceSet = "main"
38 | }
39 |
--------------------------------------------------------------------------------
/sample/android-app/src/main/java/com/icerockdev/LoadingUnitItem.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package com.icerockdev
6 |
7 | import android.view.LayoutInflater
8 | import android.view.ViewGroup
9 | import androidx.lifecycle.LifecycleOwner
10 | import androidx.recyclerview.widget.RecyclerView
11 | import dev.icerock.moko.units.TableUnitItem
12 |
13 | class LoadingUnitItem : TableUnitItem {
14 | override val itemId: Long = -2 // we have only one loading in list
15 | override val viewType: Int = R.layout.unit_loading
16 |
17 | override fun createViewHolder(
18 | parent: ViewGroup,
19 | lifecycleOwner: LifecycleOwner
20 | ): RecyclerView.ViewHolder {
21 | val inflater = LayoutInflater.from(parent.context)
22 | val loadingView = inflater.inflate(R.layout.unit_loading, parent, false)
23 | return object : RecyclerView.ViewHolder(loadingView) {}
24 | }
25 |
26 | override fun bindViewHolder(viewHolder: RecyclerView.ViewHolder) {
27 | // do nothing - it's just loading item
28 | }
29 | }
--------------------------------------------------------------------------------
/sample/ios-app/src/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "scale" : "1x",
6 | "size" : "16x16"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "2x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "1x",
16 | "size" : "32x32"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "2x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "1x",
26 | "size" : "128x128"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "2x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "1x",
36 | "size" : "256x256"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "2x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "1x",
46 | "size" : "512x512"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "2x",
51 | "size" : "512x512"
52 | }
53 | ],
54 | "info" : {
55 | "version" : 1,
56 | "author" : "xcode"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Create release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | version:
7 | description: 'Version'
8 | default: '0.1.0'
9 | required: true
10 |
11 | jobs:
12 | publish:
13 | name: Publish library at mavenCentral
14 | runs-on: macos-latest
15 | env:
16 | OSSRH_USER: ${{ secrets.OSSRH_USER }}
17 | OSSRH_KEY: ${{ secrets.OSSRH_KEY }}
18 | SIGNING_KEY_ID: ${{ secrets.SIGNING_KEYID }}
19 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
20 | SIGNING_KEY: ${{ secrets.GPG_KEY_CONTENTS }}
21 | steps:
22 | - uses: actions/checkout@v1
23 | - name: Set up JDK 11
24 | uses: actions/setup-java@v1
25 | with:
26 | java-version: 11
27 | - name: Publish
28 | run: ./gradlew publish
29 | release:
30 | name: Create release
31 | needs: publish
32 | runs-on: ubuntu-latest
33 | steps:
34 | - name: Create Release
35 | id: create_release
36 | uses: actions/create-release@v1
37 | env:
38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39 | with:
40 | commitish: ${{ github.ref }}
41 | tag_name: release/${{ github.event.inputs.version }}
42 | release_name: ${{ github.event.inputs.version }}
43 | body: "Will be filled later"
44 | draft: true
--------------------------------------------------------------------------------
/.github/workflows/compilation-check.yml:
--------------------------------------------------------------------------------
1 | name: KMP library compilation check
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - master
7 | - develop
8 |
9 | jobs:
10 | test:
11 | runs-on: macOS-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v1
15 | - name: Set up JDK 11
16 | uses: actions/setup-java@v1
17 | with:
18 | java-version: 11
19 | - name: Gradle Build Action
20 | uses: gradle/gradle-build-action@v2.1.5
21 | - name: Check runtime
22 | run: ./gradlew build publishToMavenLocal syncMultiPlatformLibraryDebugFrameworkIosX64
23 | - name: Install pods
24 | run: cd sample/ios-app && pod install
25 | - name: build ios sample
26 | run: cd sample/ios-app && set -o pipefail && xcodebuild -scheme TestProj -workspace TestProj.xcworkspace -configuration Debug build CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO
27 | - name: "Comment build scan url"
28 | uses: actions/github-script@v5
29 | if: github.event_name == 'pull_request' && failure()
30 | with:
31 | github-token: ${{secrets.GITHUB_TOKEN}}
32 | script: |
33 | github.rest.issues.createComment({
34 | issue_number: context.issue.number,
35 | owner: context.repo.owner,
36 | repo: context.repo.repo,
37 | body: '❌ ${{ github.workflow }} failed: ${{ steps.gradle.outputs.build-scan-url }}'
38 | })
39 |
--------------------------------------------------------------------------------
/paging/src/commonMain/kotlin/dev/icerock/moko/paging/ReachEndNotifierList.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.paging
6 |
7 | @Suppress("TooManyFunctions")
8 | class ReachEndNotifierList(
9 | private val mWrappedList: List,
10 | val onReachEnd: (Int) -> Unit
11 | ) : List {
12 |
13 | override val size: Int = mWrappedList.size
14 |
15 | override fun contains(element: T): Boolean = mWrappedList.contains(element)
16 |
17 | override fun containsAll(elements: Collection): Boolean = mWrappedList.containsAll(elements)
18 |
19 | override fun get(index: Int): T = mWrappedList[index]
20 |
21 | override fun indexOf(element: T): Int = mWrappedList.indexOf(element)
22 |
23 | override fun isEmpty(): Boolean = mWrappedList.isEmpty()
24 |
25 | override fun iterator(): Iterator = mWrappedList.iterator()
26 |
27 | override fun lastIndexOf(element: T): Int = mWrappedList.lastIndexOf(element)
28 |
29 | override fun listIterator(): ListIterator = mWrappedList.listIterator()
30 |
31 | override fun listIterator(index: Int): ListIterator = mWrappedList.listIterator(index)
32 |
33 | override fun subList(fromIndex: Int, toIndex: Int): List =
34 | mWrappedList.subList(fromIndex, toIndex)
35 |
36 | fun notifyReachEnd() {
37 | onReachEnd(mWrappedList.lastIndex)
38 | }
39 | }
40 |
41 | fun List.withReachEndNotifier(action: (Int) -> Unit): ReachEndNotifierList =
42 | ReachEndNotifierList(
43 | this,
44 | action
45 | )
46 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Do’s and Don’ts
2 |
3 | * **Search tickets before you file a new one.** Add to tickets if you have new information about the issue.
4 | * **Keep tickets short but sweet.** Make sure you include all the context needed to solve the issue. Don't overdo it. Great tickets allow us to focus on solving problems instead of discussing them.
5 | * **Take care of your ticket.** When you spend time to report a ticket with care we'll enjoy fixing it for you.
6 | * **Use [GitHub-flavored Markdown](https://help.github.com/articles/markdown-basics/).** Especially put code blocks and console outputs in backticks (```` ``` ````). That increases the readability. Bonus points for applying the appropriate syntax highlighting.
7 |
8 | ## Bug Reports
9 |
10 | In short, since you are most likely a developer, provide a ticket that you _yourself_ would _like_ to receive.
11 |
12 | First check if you are using the latest library version and Kotlin version before filing a ticket.
13 |
14 | Please include steps to reproduce and _all_ other relevant information, including any other relevant dependency and version information.
15 |
16 | ## Feature Requests
17 |
18 | Please try to be precise about the proposed outcome of the feature and how it
19 | would related to existing features.
20 |
21 |
22 | ## Pull Requests
23 |
24 | We **love** pull requests!
25 |
26 | All contributions _will_ be licensed under the Apache 2 license.
27 |
28 | Code/comments should adhere to the following rules:
29 |
30 | * Names should be descriptive and concise.
31 | * Use four spaces and no tabs.
32 | * Remember that source code usually gets written once and read often: ensure
33 | the reader doesn't have to make guesses. Make sure that the purpose and inner
34 | logic are either obvious to a reasonably skilled professional, or add a
35 | comment that explains it.
36 | * Please add a detailed description.
37 |
38 | If you consistently contribute improvements and/or bug fixes, we're happy to make you a maintainer.
--------------------------------------------------------------------------------
/sample/ios-app/src/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | $(PRODUCT_NAME)
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleLocalizations
16 |
17 | en
18 | es
19 | ru
20 |
21 | CFBundleName
22 | $(BUNDLE_NAME)
23 | CFBundlePackageType
24 | APPL
25 | CFBundleShortVersionString
26 | 0.1.0
27 | CFBundleVersion
28 | 1
29 | LSApplicationCategoryType
30 | public.app-category.developer-tools
31 | LSRequiresIPhoneOS
32 |
33 | NSAppTransportSecurity
34 |
35 | NSAllowsArbitraryLoads
36 |
37 |
38 | NSMainStoryboardFile
39 | Main
40 | UILaunchStoryboardName
41 | LaunchScreen
42 | UIMainStoryboardFile
43 | Main
44 | UIRequiredDeviceCapabilities
45 |
46 | armv7
47 |
48 | UIRequiresFullScreen
49 |
50 | UIStatusBarHidden
51 |
52 | UIStatusBarHidden~ipad
53 |
54 | UIStatusBarStyle
55 | UIStatusBarStyleLightContent
56 | UISupportedInterfaceOrientations
57 |
58 | UIInterfaceOrientationPortrait
59 |
60 | UISupportedInterfaceOrientations~ipad
61 |
62 | UIInterfaceOrientationPortrait
63 | UIInterfaceOrientationLandscapeRight
64 | UIInterfaceOrientationLandscapeLeft
65 | UIInterfaceOrientationPortraitUpsideDown
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/sample/mpp-library/MultiPlatformLibrary.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |spec|
2 | spec.name = 'MultiPlatformLibrary'
3 | spec.version = '0.1.0'
4 | spec.homepage = 'Link to a Kotlin/Native module homepage'
5 | spec.source = { :git => "Not Published", :tag => "Cocoapods/#{spec.name}/#{spec.version}" }
6 | spec.authors = 'IceRock Development'
7 | spec.license = ''
8 | spec.summary = 'Shared code between iOS and Android'
9 |
10 | spec.vendored_frameworks = "build/cocoapods/framework/#{spec.name}.framework"
11 | spec.libraries = "c++"
12 | spec.module_name = "#{spec.name}_umbrella"
13 |
14 | spec.ios.deployment_target = '11.0'
15 | spec.osx.deployment_target = '10.6'
16 |
17 | spec.pod_target_xcconfig = {
18 | 'KOTLIN_FRAMEWORK_BUILD_TYPE[config=*ebug]' => 'debug',
19 | 'KOTLIN_FRAMEWORK_BUILD_TYPE[config=*elease]' => 'release',
20 | 'CURENT_SDK[sdk=iphoneos*]' => 'iphoneos',
21 | 'CURENT_SDK[sdk=iphonesimulator*]' => 'iphonesimulator',
22 | 'CURENT_SDK[sdk=macosx*]' => 'macos'
23 | }
24 |
25 | spec.script_phases = [
26 | {
27 | :name => 'Compile Kotlin/Native',
28 | :execution_position => :before_compile,
29 | :shell_path => '/bin/sh',
30 | :script => <<-SCRIPT
31 | if [ "$KOTLIN_FRAMEWORK_BUILD_TYPE" == "debug" ]; then
32 | CONFIG="Debug"
33 | else
34 | CONFIG="Release"
35 | fi
36 |
37 | if [ "$CURENT_SDK" == "iphoneos" ]; then
38 | TARGET="Ios"
39 | ARCH="Arm64"
40 | elif [ "$CURENT_SDK" == "macos" ]; then
41 | TARGET="Macos"
42 | if [ "$NATIVE_ARCH" == "arm64" ]; then
43 | ARCH="Arm64"
44 | else
45 | ARCH="X64"
46 | fi
47 | else
48 | if [ "$NATIVE_ARCH" == "arm64" ]; then
49 | TARGET="IosSimulator"
50 | ARCH="Arm64"
51 | else
52 | TARGET="Ios"
53 | ARCH="X64"
54 | fi
55 | fi
56 |
57 | MPP_PROJECT_ROOT="$SRCROOT/../../mpp-library"
58 | GRADLE_TASK="syncMultiPlatformLibrary${CONFIG}Framework${TARGET}${ARCH}"
59 |
60 | "$MPP_PROJECT_ROOT/../gradlew" -p "$MPP_PROJECT_ROOT" "$GRADLE_TASK"
61 | SCRIPT
62 | }
63 | ]
64 | end
--------------------------------------------------------------------------------
/sample/ios-app/src/Resources/ProductTableViewCell.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | kotlinVersion = "1.6.20"
3 | androidAppCompatVersion = "1.2.0"
4 | androidLifecycleVersion = "2.1.0"
5 | androidCoreTestingVersion = "2.1.0"
6 | recyclerViewVersion = "1.1.0"
7 | swipeRefreshLayoutVersion = "1.1.0"
8 | ktorClientVersion = "2.0.0"
9 | coroutinesVersion = "1.6.0-native-mt"
10 | mokoMvvmVersion = "0.12.0"
11 | mokoResourcesVersion = "0.18.0"
12 | mokoUnitsVersion = "0.8.0"
13 | mokoPagingVersion = "0.7.2"
14 |
15 | [libraries]
16 | appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" }
17 | recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerViewVersion" }
18 | lifecycle = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = "androidLifecycleVersion" }
19 | swipeRefreshLayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swipeRefreshLayoutVersion" }
20 | coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutinesVersion" }
21 | ktorClient = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientVersion" }
22 | mokoResources = { module = "dev.icerock.moko:resources", version.ref = "mokoResourcesVersion" }
23 | mokoUnits = { module = "dev.icerock.moko:units", version.ref = "mokoUnitsVersion" }
24 | mokoUnitsDataBinding = { module = "dev.icerock.moko:units-databinding", version.ref = "mokoUnitsVersion" }
25 | mokoMvvmLiveData = { module = "dev.icerock.moko:mvvm-livedata", version.ref = "mokoMvvmVersion" }
26 | mokoMvvmState = { module = "dev.icerock.moko:mvvm-state", version.ref = "mokoMvvmVersion" }
27 | kotlinTestJUnit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlinVersion" }
28 | androidCoreTesting = { module = "androidx.arch.core:core-testing", version.ref = "androidCoreTestingVersion" }
29 | ktorClientMock = { module = "io.ktor:ktor-client-mock", version.ref = "ktorClientVersion" }
30 |
31 | kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlinVersion" }
32 | androidGradlePlugin = { module = "com.android.tools.build:gradle", version = "7.0.4" }
33 | googleServicesGradlePlugin = { module = "com.google.gms:google-services", version = "4.3.8" }
34 | firebaseGradlePlugin = { module = "com.google.firebase:firebase-crashlytics-gradle", version = "2.2.0" }
35 | mokoGradlePlugin = { module = "dev.icerock.moko:moko-gradle-plugin", version = "0.1.0" }
36 | mobileMultiplatformGradlePlugin = { module = "dev.icerock:mobile-multiplatform", version = "0.14.1" }
37 | kotlinSerializationGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlinVersion" }
38 | mokoUnitsGeneratorGradlePlugin = { module = "dev.icerock.moko:units-generator", version.ref = "mokoUnitsVersion" }
39 |
--------------------------------------------------------------------------------
/paging/src/iosTest/kotlin/dev/icerock/moko/paging/Utils.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.paging
6 |
7 | import kotlinx.coroutines.CancellableContinuation
8 | import kotlinx.coroutines.CoroutineDispatcher
9 | import kotlinx.coroutines.CoroutineScope
10 | import kotlinx.coroutines.Delay
11 | import kotlinx.coroutines.DisposableHandle
12 | import kotlinx.coroutines.GlobalScope
13 | import kotlinx.coroutines.InternalCoroutinesApi
14 | import kotlinx.coroutines.Runnable
15 | import kotlinx.coroutines.launch
16 | import platform.Foundation.NSDate
17 | import platform.Foundation.NSDefaultRunLoopMode
18 | import platform.Foundation.NSRunLoop
19 | import platform.Foundation.NSTimer
20 | import platform.Foundation.addTimeInterval
21 | import platform.Foundation.performBlock
22 | import platform.Foundation.runUntilDate
23 | import kotlin.coroutines.CoroutineContext
24 |
25 | actual fun runTest(block: suspend CoroutineScope.() -> T): T {
26 | val expectation = Expectation>()
27 |
28 | GlobalScope.launch(MainRunLoopDispatcher) {
29 | expectation.fulfill(kotlin.runCatching { block.invoke(this) })
30 | }
31 |
32 | val result: Result? = expectation.wait()
33 | if (result == null) throw RuntimeException("runBlocking failed")
34 |
35 | return result.getOrThrow()
36 | }
37 |
38 | private class Expectation {
39 | private var waiting = true
40 | private var result: T? = null
41 |
42 | fun fulfill(result: T?) {
43 | waiting = false
44 | this.result = result
45 | }
46 |
47 | fun wait(): T? {
48 | while (waiting) {
49 | advanceRunLoop()
50 | }
51 |
52 | return result
53 | }
54 |
55 | fun advanceRunLoop() {
56 | val date = NSDate().addTimeInterval(1.0) as NSDate
57 | NSRunLoop.mainRunLoop.runUntilDate(date)
58 | }
59 | }
60 |
61 | @OptIn(InternalCoroutinesApi::class)
62 | private object MainRunLoopDispatcher : CoroutineDispatcher(), Delay {
63 | override fun dispatch(context: CoroutineContext, block: Runnable) {
64 | NSRunLoop.mainRunLoop.performBlock {
65 | block.run()
66 | }
67 | }
68 |
69 | override fun scheduleResumeAfterDelay(
70 | timeMillis: Long,
71 | continuation: CancellableContinuation
72 | ) {
73 | val timer = NSTimer(
74 | fireDate = NSDate().addTimeInterval((timeMillis / 1000).toDouble()) as NSDate,
75 | interval = 0.0,
76 | repeats = false,
77 | block = {
78 | val result = continuation.tryResume(Unit)
79 | if (result != null) continuation.completeResume(result)
80 | }
81 | )
82 | NSRunLoop.mainRunLoop.addTimer(timer, NSDefaultRunLoopMode)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/sample/android-app/src/main/java/com/icerockdev/MainActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package com.icerockdev
6 |
7 | import android.os.Bundle
8 | import android.view.View
9 | import androidx.appcompat.app.AppCompatActivity
10 | import androidx.lifecycle.Observer
11 | import androidx.recyclerview.widget.LinearLayoutManager
12 | import androidx.recyclerview.widget.RecyclerView
13 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
14 | import com.icerockdev.library.ListViewModel
15 | import dev.icerock.moko.mvvm.getViewModel
16 | import dev.icerock.moko.mvvm.livedata.data
17 | import dev.icerock.moko.units.TableUnitItem
18 | import dev.icerock.moko.units.adapter.UnitsRecyclerViewAdapter
19 |
20 | class MainActivity : AppCompatActivity() {
21 |
22 | private val unitsFactory = object : ListViewModel.UnitsFactory {
23 | override fun createProductUnit(id: Long, title: String): TableUnitItem {
24 | // databinding generated unit
25 | return UnitProduct().also {
26 | it.itemId = id
27 | it.title = title
28 | }
29 | }
30 |
31 | override fun createLoading(): TableUnitItem {
32 | // manual created unit
33 | return LoadingUnitItem()
34 | }
35 | }
36 |
37 | override fun onCreate(savedInstanceState: Bundle?) {
38 | super.onCreate(savedInstanceState)
39 |
40 | setContentView(R.layout.activity_main)
41 |
42 | val viewModel = getViewModel { ListViewModel(unitsFactory) }
43 | val unitsAdapter = UnitsRecyclerViewAdapter(this)
44 | val swipeRefreshLayout = findViewById(R.id.swipeRefresh)
45 | val recyclerView = findViewById(R.id.recyclerView)
46 |
47 | with(recyclerView) {
48 | layoutManager = LinearLayoutManager(this@MainActivity, RecyclerView.VERTICAL, false)
49 | adapter = unitsAdapter
50 | }
51 |
52 | viewModel.isRefreshing.ld().observe(this, Observer { swipeRefreshLayout.isRefreshing = it })
53 | viewModel.state.data().ld().observe(this, Observer { unitsAdapter.units = it.orEmpty() })
54 |
55 | swipeRefreshLayout.setOnRefreshListener { viewModel.onRefresh() }
56 |
57 | recyclerView.addOnChildAttachStateChangeListener(object : RecyclerView.OnChildAttachStateChangeListener {
58 | override fun onChildViewDetachedFromWindow(view: View) {}
59 |
60 | override fun onChildViewAttachedToWindow(view: View) {
61 | val count = unitsAdapter.itemCount
62 | val position = recyclerView.getChildAdapterPosition(view)
63 | if (position != count - 1) return
64 |
65 | viewModel.onLoadNextPage()
66 | }
67 | })
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/sample/ios-app/src/Resources/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/sample/ios-app/src/Resources/LoadingTableViewCell.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/sample/ios-app/src/TestViewController.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | import UIKit
6 | import MultiPlatformLibrary
7 | import MultiPlatformLibraryUnits
8 |
9 | class TestViewController: UIViewController {
10 |
11 | @IBOutlet private var tableView: UITableView!
12 |
13 | private var listViewModel: ListViewModel!
14 |
15 | override func viewDidLoad() {
16 | super.viewDidLoad()
17 |
18 | let unitsSource = TableUnitsSourceKt.default(for: tableView)
19 | listViewModel = ListViewModel(unitsFactory: self)
20 |
21 | tableView.delegate = self
22 |
23 | let refreshControl = UIRefreshControl()
24 | refreshControl.addTarget(self, action: #selector(onRefresh), for: .valueChanged)
25 | tableView.refreshControl = refreshControl
26 |
27 |
28 | listViewModel.state.data().addObserver { data in
29 | guard let units = data as? [TableUnitItem] else { return }
30 |
31 | unitsSource.unitItems = units
32 | }
33 | listViewModel.isRefreshing.addObserver { refreshing in
34 | if refreshing == false {
35 | refreshControl.endRefreshing()
36 | }
37 | }
38 | }
39 |
40 | @objc func onRefresh() {
41 | listViewModel.onRefresh()
42 | }
43 | }
44 |
45 | extension TestViewController: ListViewModelUnitsFactory {
46 | func createLoading() -> TableUnitItem {
47 | return UITableViewCellUnit(
48 | data: Void(),
49 | itemId: -2,
50 | configurator: nil
51 | )
52 | }
53 |
54 | func createProductUnit(id: Int64, title: String) -> TableUnitItem {
55 | return UITableViewCellUnit(
56 | data: title,
57 | itemId: id,
58 | configurator: nil
59 | )
60 | }
61 | }
62 |
63 | extension TestViewController: UITableViewDelegate {
64 | func tableView(
65 | _ tableView: UITableView,
66 | willDisplay cell: UITableViewCell,
67 | forRowAt indexPath: IndexPath
68 | ) {
69 | let lastSectionIndex = tableView.numberOfSections - 1
70 | let lastRowIndex = tableView.numberOfRows(inSection: lastSectionIndex) - 1
71 | if indexPath.section == lastSectionIndex && indexPath.row == lastRowIndex {
72 | listViewModel.onLoadNextPage()
73 | }
74 | }
75 | }
76 |
77 | class LoadingTableViewCell: UITableViewCell, Fillable, Reusable {
78 | @IBOutlet private var activityIndicator: UIActivityIndicatorView!
79 |
80 | typealias DataType = Void
81 |
82 | static func reusableIdentifier() -> String {
83 | "loading"
84 | }
85 |
86 | static func xibName() -> String {
87 | "LoadingTableViewCell"
88 | }
89 |
90 | func fill(_ data: Void) {}
91 |
92 | override func prepareForReuse() {
93 | activityIndicator.startAnimating()
94 | }
95 | }
96 |
97 | class ProductTableViewCell: UITableViewCell, Fillable, Reusable {
98 | @IBOutlet private var titleLabel: UILabel!
99 |
100 | typealias DataType = String
101 |
102 | static func reusableIdentifier() -> String {
103 | "product"
104 | }
105 |
106 | static func xibName() -> String {
107 | "ProductTableViewCell"
108 | }
109 |
110 | func fill(_ data: String) {
111 | titleLabel.text = data
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/ListViewModel.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package com.icerockdev.library
6 |
7 | import dev.icerock.moko.mvvm.ResourceState
8 | import dev.icerock.moko.mvvm.livedata.LiveData
9 | import dev.icerock.moko.mvvm.livedata.dataTransform
10 | import dev.icerock.moko.mvvm.livedata.errorTransform
11 | import dev.icerock.moko.mvvm.livedata.map
12 | import dev.icerock.moko.mvvm.livedata.mediatorOf
13 | import dev.icerock.moko.mvvm.viewmodel.ViewModel
14 | import dev.icerock.moko.paging.IdComparator
15 | import dev.icerock.moko.paging.IdEntity
16 | import dev.icerock.moko.paging.LambdaPagedListDataSource
17 | import dev.icerock.moko.paging.Pagination
18 | import dev.icerock.moko.units.TableUnitItem
19 | import kotlinx.coroutines.delay
20 |
21 | private const val PAGE_LOAD_DURATION_MS: Long = 2000
22 |
23 | class ListViewModel(
24 | private val unitsFactory: UnitsFactory
25 | ) : ViewModel() {
26 | private val pagination: Pagination = Pagination(
27 | parentScope = viewModelScope,
28 | dataSource = LambdaPagedListDataSource {
29 | delay(PAGE_LOAD_DURATION_MS)
30 |
31 | it?.plus(generatePack(it.size.toLong())) ?: generatePack()
32 | },
33 | comparator = IdComparator(),
34 | nextPageListener = ::onNextPageResult,
35 | refreshListener = ::onRefreshResult,
36 | initValue = generatePack()
37 | )
38 |
39 | val isRefreshing: LiveData = pagination.refreshLoading
40 | val state: LiveData, String>> = pagination.state
41 | .dataTransform {
42 | mediatorOf(
43 | this.map { productList ->
44 | productList.map { product ->
45 | unitsFactory.createProductUnit(
46 | id = product.id,
47 | title = product.title
48 | )
49 | }
50 | },
51 | pagination.nextPageLoading
52 | ) { items, nextPageLoading ->
53 | if (nextPageLoading) {
54 | items.plus(unitsFactory.createLoading())
55 | } else {
56 | items
57 | }
58 | }
59 | }
60 | .errorTransform {
61 | map { it.toString() }
62 | }
63 |
64 | fun onRetryPressed() {
65 | pagination.loadFirstPage()
66 | }
67 |
68 | fun onLoadNextPage() {
69 | pagination.loadNextPage()
70 | }
71 |
72 | fun onRefresh() {
73 | pagination.refresh()
74 | }
75 |
76 | private fun onNextPageResult(result: Result>) {
77 | if (result.isSuccess) {
78 | println("next page successful loaded")
79 | } else {
80 | println("next page loading failed ${result.exceptionOrNull()}")
81 | }
82 | }
83 |
84 | private fun onRefreshResult(result: Result>) {
85 | if (result.isSuccess) {
86 | println("refresh successful")
87 | } else {
88 | println("refresh failed ${result.exceptionOrNull()}")
89 | }
90 | }
91 |
92 | @Suppress("MagicNumber")
93 | private fun generatePack(startId: Long = 0): List {
94 | return List(20) { idx ->
95 | val id = startId + idx
96 | Product(
97 | id = id,
98 | title = "Product $id"
99 | )
100 | }
101 | }
102 |
103 | data class Product(
104 | override val id: Long,
105 | val title: String
106 | ) : IdEntity
107 |
108 | interface UnitsFactory {
109 | fun createProductUnit(id: Long, title: String): TableUnitItem
110 | fun createLoading(): TableUnitItem
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/paging/src/commonTest/kotlin/dev/icerock/moko/paging/PaginationTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.paging
6 |
7 | import kotlinx.coroutines.CoroutineScope
8 | import kotlinx.coroutines.async
9 | import kotlinx.coroutines.delay
10 | import kotlin.test.BeforeTest
11 | import kotlin.test.Test
12 | import kotlin.test.assertTrue
13 |
14 | class PaginationTest : BaseTestsClass() {
15 |
16 | var paginationDataSource = TestListDataSource(3, 5)
17 |
18 | val itemsComparator = Comparator { a: Int, b: Int ->
19 | a - b
20 | }
21 |
22 | @BeforeTest
23 | fun setup() {
24 | paginationDataSource = TestListDataSource(3, 5)
25 | }
26 |
27 | @Test
28 | fun `load first page`() = runTest {
29 | val pagination = createPagination()
30 |
31 | pagination.loadFirstPageSuspend()
32 |
33 | assertTrue {
34 | pagination.state.value.isSuccess()
35 | }
36 | assertTrue {
37 | pagination.state.value.dataValue()!!.compareWith(listOf(0, 1, 2))
38 | }
39 | }
40 |
41 | @Test
42 | fun `load next page`() = runTest {
43 | val pagination = createPagination()
44 |
45 | pagination.loadFirstPageSuspend()
46 | pagination.loadNextPageSuspend()
47 |
48 | assertTrue {
49 | pagination.state.value.dataValue()!!.compareWith(listOf(0, 1, 2, 3, 4, 5))
50 | }
51 |
52 | pagination.loadNextPageSuspend()
53 |
54 | assertTrue {
55 | pagination.state.value.dataValue()!!.compareWith(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8))
56 | }
57 | }
58 |
59 | @Test
60 | fun `refresh pagination`() = runTest {
61 | val pagination = createPagination()
62 |
63 | pagination.loadFirstPageSuspend()
64 | pagination.loadNextPageSuspend()
65 | pagination.refreshSuspend()
66 |
67 | assertTrue {
68 | pagination.state.value.dataValue()!!.compareWith(listOf(0, 1, 2))
69 | }
70 | }
71 |
72 | @Test
73 | fun `set data`() = runTest {
74 | val pagination = createPagination()
75 |
76 | pagination.loadFirstPageSuspend()
77 | pagination.loadNextPageSuspend()
78 |
79 | val setList = listOf(5, 2, 3, 1, 4)
80 | pagination.setDataSuspend(setList)
81 |
82 | assertTrue {
83 | pagination.state.value.dataValue()!!.compareWith(setList)
84 | }
85 | }
86 |
87 | @Test
88 | fun `double refresh`() = runTest {
89 | var counter = 0
90 | val pagination = Pagination(
91 | parentScope = this,
92 | dataSource = LambdaPagedListDataSource {
93 | val load = counter++
94 | println("start load new page with $it")
95 | delay(100)
96 | println("respond new list $load")
97 | listOf(1, 2, 3, 4)
98 | },
99 | comparator = itemsComparator,
100 | nextPageListener = { },
101 | refreshListener = { }
102 | )
103 |
104 | println("start load first page")
105 | pagination.loadFirstPageSuspend()
106 | println("end load first page")
107 |
108 | println("start double refresh")
109 | val r1 = async {
110 | pagination.refreshSuspend()
111 | println("first refresh end")
112 | }
113 | val r2 = async {
114 | pagination.refreshSuspend()
115 | println("second refresh end")
116 | }
117 |
118 | r1.await()
119 | r2.await()
120 | }
121 |
122 | private fun CoroutineScope.createPagination(
123 | nextPageListener: (Result>) -> Unit = {},
124 | refreshListener: (Result>) -> Unit = {}
125 | ) = Pagination(
126 | parentScope = this,
127 | dataSource = paginationDataSource,
128 | comparator = itemsComparator,
129 | nextPageListener = nextPageListener,
130 | refreshListener = refreshListener
131 | )
132 | }
133 |
--------------------------------------------------------------------------------
/sample/ios-app/src/Resources/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | [](http://www.apache.org/licenses/LICENSE-2.0) [ ](https://repo1.maven.org/maven2/dev/icerock/moko/paging) 
3 |
4 | # Mobile Kotlin paging
5 | This is a Kotlin MultiPlatform library that contains pagination logic for kotlin multiplatform
6 |
7 | ## Table of Contents
8 | - [Features](#features)
9 | - [Requirements](#requirements)
10 | - [Installation](#installation)
11 | - [Usage](#usage)
12 | - [Samples](#samples)
13 | - [Set Up Locally](#set-up-locally)
14 | - [Contributing](#contributing)
15 | - [License](#license)
16 |
17 | ## Features
18 | - **Pagination** implements pagination logic for the data from abstract `PagedListDataSource`.
19 | - Managing a data loading process using **Pagination** asynchronous functions: `loadFirstPage`, `loadNextPage`,
20 | `refresh` or their duplicates with `suspend` modifier.
21 | - Observing states of **Pagination** using `LiveData` from **moko-mvvm**.
22 |
23 | ## Requirements
24 | - Gradle version 6.8+
25 | - Android API 16+
26 | - iOS version 11.0+
27 |
28 | ## Installation
29 | root build.gradle
30 | ```groovy
31 | allprojects {
32 | repositories {
33 | mavenCentral()
34 | }
35 | }
36 | ```
37 |
38 | project build.gradle
39 | ```groovy
40 | dependencies {
41 | commonMainApi("dev.icerock.moko:paging:0.7.1")
42 | }
43 | ```
44 |
45 | ## Usage
46 |
47 | You can use **Pagination** in `commonMain` sourceset.
48 |
49 | **Pagination** creation:
50 |
51 | ```kotlin
52 | val pagination: Pagination = Pagination(
53 | parentScope = coroutineScope,
54 | dataSource = LambdaPagedListDataSource { currentList ->
55 | extrenalRepository.loadPage(currentList)
56 | },
57 | comparator = Comparator { a: Int, b: Int ->
58 | a - b
59 | },
60 | nextPageListener = { result: Result> ->
61 | if (result.isSuccess) {
62 | println("Next page successful loaded")
63 | } else {
64 | println("Next page loading failed")
65 | }
66 | },
67 | refreshListener = { result: Result> ->
68 | if (result.isSuccess) {
69 | println("Refresh successful")
70 | } else {
71 | println("Refresh failed")
72 | }
73 | },
74 | initValue = listOf(1, 2, 3)
75 | )
76 | ```
77 |
78 | Managing data loading:
79 |
80 | ```kotlin
81 | // Loading first page
82 | pagination.loadFirstPage()
83 |
84 | // Loading next page
85 | pagination.loadNextPage()
86 |
87 | // Refreshing pagnation
88 | pagination.refresh()
89 |
90 | // Setting new list
91 | pagination.setData(itemsList)
92 | ```
93 |
94 | Observing **Pagination** states:
95 |
96 | ```kotlin
97 | // Observing the state of the pagination
98 | pagination.state.addObserver { state: ResourceState, Throwable> ->
99 | // ...
100 | }
101 |
102 | // Observing the next page loading process
103 | pagination.nextPageLoading.addObserver { isLoading: Boolean ->
104 | // ...
105 | }
106 |
107 | // Observing the refresh process
108 | pagination.refreshLoading.addObserver { isRefreshing: Boolean ->
109 | // ...
110 | }
111 | ```
112 |
113 | ## Samples
114 | Please see more examples in the [sample directory](sample).
115 |
116 | ## Set Up Locally
117 | - The [paging directory](paging) contains the `paging` library;
118 | - The [sample directory](sample) contains sample apps for Android and iOS; plus the mpp-library connected to the apps.
119 |
120 | ## Contributing
121 | All development (both new features and bug fixes) is performed in the `develop` branch. This way `master` always contains the sources of the most recently released version. Please send PRs with bug fixes to the `develop` branch. Documentation fixes in the markdown files are an exception to this rule. They are updated directly in `master`.
122 |
123 | The `develop` branch is pushed to `master` on release.
124 |
125 | For more details on contributing please see the [contributing guide](CONTRIBUTING.md).
126 |
127 | ## License
128 |
129 | Copyright 2020 IceRock MAG Inc.
130 |
131 | Licensed under the Apache License, Version 2.0 (the "License");
132 | you may not use this file except in compliance with the License.
133 | You may obtain a copy of the License at
134 |
135 | http://www.apache.org/licenses/LICENSE-2.0
136 |
137 | Unless required by applicable law or agreed to in writing, software
138 | distributed under the License is distributed on an "AS IS" BASIS,
139 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
140 | See the License for the specific language governing permissions and
141 | limitations under the License.
142 |
--------------------------------------------------------------------------------
/paging/src/commonTest/kotlin/dev/icerock/moko/paging/IntegrationTests.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.paging
6 |
7 | import io.ktor.client.HttpClient
8 | import io.ktor.client.engine.mock.MockEngine
9 | import io.ktor.client.engine.mock.respondOk
10 | import io.ktor.client.request.get
11 | import io.ktor.client.statement.*
12 | import io.ktor.http.fullPath
13 | import kotlinx.coroutines.async
14 | import kotlinx.coroutines.cancel
15 | import kotlinx.coroutines.coroutineScope
16 | import kotlinx.coroutines.delay
17 | import kotlinx.coroutines.launch
18 | import kotlin.test.Test
19 |
20 | class IntegrationTests : BaseTestsClass() {
21 | private val httpClient = HttpClient(MockEngine) {
22 | engine {
23 | addHandler { request ->
24 | when (request.url.fullPath) {
25 | "http://api.icndb.com/jokes/random" -> {
26 | delay(200)
27 | respondOk("""
28 | {
29 | "type": "success",
30 | "value": {
31 | "id": 318,
32 | "joke": "If you work in an office with Chuck Norris, don't ask him for his three-hole-punch.",
33 | "categories": []
34 | }
35 | }
36 | """.trimIndent())
37 | }
38 | else -> error("Unhandled ${request.url.fullPath}")
39 | }
40 | }
41 | }
42 | }
43 |
44 | @Test
45 | fun parallelRequests() = runTest {
46 | val pagination = Pagination(
47 | parentScope = this,
48 | dataSource = LambdaPagedListDataSource {
49 | println("start load new page with $it")
50 | val randomJoke: String = httpClient
51 | .get("http://api.icndb.com/jokes/random")
52 | .bodyAsText()
53 |
54 | println("respond new item $randomJoke")
55 | listOf(randomJoke)
56 | },
57 | comparator = Comparator { a, b -> a.compareTo(b) },
58 | nextPageListener = { },
59 | refreshListener = { }
60 | )
61 |
62 | for (i in 0..10) {
63 | println("--- ITERATION $i START ---")
64 | println("start load first page")
65 | pagination.loadFirstPageSuspend()
66 | println("end load first page")
67 |
68 | (0..3).flatMap {
69 | listOf(
70 | async {
71 | println("--> $it refresh start")
72 | pagination.refreshSuspend()
73 | println("<-- $it refresh end")
74 | },
75 | async {
76 | println("--> $it load next page start")
77 | pagination.loadNextPageSuspend()
78 | println("<-- $it load next page end")
79 | }
80 | )
81 | }.forEach { it.await() }
82 | }
83 | }
84 |
85 | @Test
86 | fun parallelRequestsAndSetData() = runTest {
87 | val pagination = Pagination(
88 | parentScope = this,
89 | dataSource = LambdaPagedListDataSource {
90 | println("start load new page with $it")
91 | val randomJoke: String = httpClient
92 | .get("http://api.icndb.com/jokes/random")
93 | .bodyAsText()
94 |
95 | println("respond new item $randomJoke")
96 | listOf(randomJoke)
97 | },
98 | comparator = Comparator { a, b -> a.compareTo(b) },
99 | nextPageListener = { },
100 | refreshListener = { }
101 | )
102 |
103 | for (i in 0..10) {
104 | println("--- ITERATION $i START ---")
105 | println("start load first page")
106 | pagination.loadFirstPageSuspend()
107 | println("end load first page")
108 |
109 | (0..1).flatMap {
110 | listOf(
111 | async {
112 | println("--> $it refresh start")
113 | pagination.refreshSuspend()
114 | println("<-- $it refresh end")
115 | },
116 | async {
117 | println("--> $it load next page start")
118 | pagination.loadNextPageSuspend()
119 | println("<-- $it load next page end")
120 | },
121 | async {
122 | println("--> $it set data start")
123 | val data = pagination.state.value.dataValue().orEmpty()
124 | val newData = data.plus("new item")
125 | pagination.setDataSuspend(newData)
126 | println("--> $it set data end")
127 | }
128 | )
129 | }.forEach { it.await() }
130 | }
131 | }
132 |
133 | @Test
134 | fun closingScope() = runTest {
135 | val exc = runCatching {
136 | coroutineScope {
137 | val pagination = Pagination(
138 | parentScope = this,
139 | dataSource = LambdaPagedListDataSource {
140 | println("start load new page with $it")
141 | val randomJoke: String = httpClient
142 | .get("http://api.icndb.com/jokes/random")
143 | .bodyAsText()
144 |
145 | println("respond new item $randomJoke")
146 | listOf(randomJoke)
147 | },
148 | comparator = Comparator { a, b -> a.compareTo(b) },
149 | nextPageListener = { },
150 | refreshListener = { }
151 | )
152 |
153 | launch {
154 | println("start load")
155 | pagination.loadFirstPageSuspend()
156 | println("end load")
157 | }
158 |
159 | delay(50)
160 | println("cancel scope")
161 | cancel()
162 | }
163 | }
164 |
165 | println(exc)
166 | }
167 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/paging/src/commonMain/kotlin/dev/icerock/moko/paging/Pagination.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.paging
6 |
7 | import dev.icerock.moko.mvvm.ResourceState
8 | import dev.icerock.moko.mvvm.asState
9 | import dev.icerock.moko.mvvm.livedata.MutableLiveData
10 | import dev.icerock.moko.mvvm.livedata.readOnly
11 | import kotlinx.coroutines.CoroutineScope
12 | import kotlinx.coroutines.Deferred
13 | import kotlinx.coroutines.async
14 | import kotlinx.coroutines.coroutineScope
15 | import kotlinx.coroutines.launch
16 | import kotlinx.coroutines.sync.Mutex
17 | import kotlin.coroutines.CoroutineContext
18 |
19 | class Pagination- (
20 | parentScope: CoroutineScope,
21 | private val dataSource: PagedListDataSource
- ,
22 | private val comparator: Comparator
- ,
23 | private val nextPageListener: (Result
>) -> Unit,
24 | private val refreshListener: (Result>) -> Unit,
25 | initValue: List- ? = null
26 | ) : CoroutineScope {
27 |
28 | override val coroutineContext: CoroutineContext = parentScope.coroutineContext
29 |
30 | private val mStateStorage =
31 | MutableLiveData, Throwable>>(initValue.asStateNullIsLoading())
32 |
33 | val state = mStateStorage.readOnly()
34 |
35 | private val mNextPageLoading = MutableLiveData(false)
36 | val nextPageLoading = mNextPageLoading.readOnly()
37 |
38 | private val mEndOfList = MutableLiveData(false)
39 |
40 | private val mRefreshLoading = MutableLiveData(false)
41 | val refreshLoading = mRefreshLoading.readOnly()
42 |
43 | private val listMutex = Mutex()
44 |
45 | private var loadNextPageDeferred: Deferred
>? = null
46 |
47 | fun loadFirstPage() {
48 | launch {
49 | loadFirstPageSuspend()
50 | }
51 | }
52 |
53 | suspend fun loadFirstPageSuspend() {
54 | loadNextPageDeferred?.cancel()
55 |
56 | listMutex.lock()
57 |
58 | mEndOfList.value = false
59 | mNextPageLoading.value = false
60 | mStateStorage.value = ResourceState.Loading()
61 |
62 | @Suppress("TooGenericExceptionCaught")
63 | try {
64 | val items: List- = dataSource.loadPage(null)
65 | mStateStorage.value = items.asState()
66 | } catch (error: Exception) {
67 | mStateStorage.value = ResourceState.Failed(error)
68 | }
69 | listMutex.unlock()
70 | }
71 |
72 | fun loadNextPage() {
73 | launch {
74 | loadNextPageSuspend()
75 | }
76 | }
77 |
78 | @Suppress("ReturnCount")
79 | suspend fun loadNextPageSuspend() {
80 | if (mNextPageLoading.value) return
81 | if (mRefreshLoading.value) return
82 | if (mEndOfList.value) return
83 |
84 | listMutex.lock()
85 |
86 | mNextPageLoading.value = true
87 |
88 | @Suppress("TooGenericExceptionCaught")
89 | try {
90 | loadNextPageDeferred = coroutineScope {
91 | async {
92 | val currentList = mStateStorage.value.dataValue()
93 | ?: throw IllegalStateException("Try to load next page when list is empty")
94 | // load next page items
95 | val items = dataSource.loadPage(currentList)
96 | // remove already exist items
97 | val newItems = items.filter { item ->
98 | val existsItem =
99 | currentList.firstOrNull { comparator.compare(item, it) == 0 }
100 | existsItem == null
101 | }
102 | // append new items to current list
103 | val newList = currentList.plus(newItems)
104 | // mark end of list if no new items
105 | if (newItems.isEmpty()) {
106 | mEndOfList.value = true
107 | } else {
108 | // save
109 | mStateStorage.value = newList.asState()
110 | }
111 | newList
112 | }
113 | }
114 | val newList = loadNextPageDeferred!!.await()
115 |
116 | // flag
117 | mNextPageLoading.value = false
118 | // notify
119 | nextPageListener(Result.success(newList))
120 | } catch (error: Exception) {
121 | // flag
122 | mNextPageLoading.value = false
123 | // notify
124 | nextPageListener(Result.failure(error))
125 | }
126 | listMutex.unlock()
127 | }
128 |
129 | fun refresh() {
130 | launch {
131 | refreshSuspend()
132 | }
133 | }
134 |
135 | suspend fun refreshSuspend() {
136 | loadNextPageDeferred?.cancel()
137 | listMutex.lock()
138 |
139 | if (mRefreshLoading.value) {
140 | listMutex.unlock()
141 | return
142 | }
143 | if (mNextPageLoading.value) {
144 | listMutex.unlock()
145 | return
146 | }
147 |
148 | mRefreshLoading.value = true
149 |
150 | @Suppress("TooGenericExceptionCaught")
151 | try {
152 | // load first page items
153 | val items = dataSource.loadPage(null)
154 | // save
155 | mStateStorage.value = items.asState()
156 | // flag
157 | mEndOfList.value = false
158 | mRefreshLoading.value = false
159 | // notify
160 | refreshListener(Result.success(items))
161 | } catch (error: Exception) {
162 | // flag
163 | mRefreshLoading.value = false
164 | // notify
165 | refreshListener(Result.failure(error))
166 | }
167 | listMutex.unlock()
168 | }
169 |
170 | fun setData(items: List
- ?) {
171 | launch {
172 | setDataSuspend(items)
173 | }
174 | }
175 |
176 | suspend fun setDataSuspend(items: List
- ?) {
177 | listMutex.lock()
178 | mStateStorage.value = items.asStateNullIsEmpty()
179 | mEndOfList.value = false
180 | listMutex.unlock()
181 | }
182 | }
183 |
184 | fun List?.asStateNullIsEmpty() = asState {
185 | ResourceState.Empty
, E>()
186 | }
187 |
188 | fun List?.asStateNullIsLoading() = asState {
189 | ResourceState.Loading, E>()
190 | }
191 |
192 | interface IdEntity {
193 | val id: Long
194 | }
195 |
196 | class IdComparator : Comparator {
197 | override fun compare(a: T, b: T): Int {
198 | return if (a.id == b.id) 0 else 1
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/sample/ios-app/TestProj.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 2B70A10DE02726CA8E6981EB /* Pods_TestProj.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E8D683A7C91DCD56058C7435 /* Pods_TestProj.framework */; };
11 | 459B259423FA5E9B00AAF627 /* LoadingTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 459B259323FA5E9B00AAF627 /* LoadingTableViewCell.xib */; };
12 | 459B259623FA5F2800AAF627 /* ProductTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 459B259523FA5F2800AAF627 /* ProductTableViewCell.xib */; };
13 | 45D74FCC22BFDDFD00CAB0C8 /* TestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45D74FCB22BFDDFD00CAB0C8 /* TestViewController.swift */; };
14 | 45F4791D219463C7003D25FA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 45F47912219463C7003D25FA /* LaunchScreen.storyboard */; };
15 | 45F4791E219463C7003D25FA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 45F47914219463C7003D25FA /* Main.storyboard */; };
16 | 45F47921219463C7003D25FA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 45F4791A219463C7003D25FA /* Assets.xcassets */; };
17 | 45F47922219463C7003D25FA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F4791B219463C7003D25FA /* AppDelegate.swift */; };
18 | /* End PBXBuildFile section */
19 |
20 | /* Begin PBXFileReference section */
21 | 287627FF1F319065007FA12B /* moko-paging.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "moko-paging.app"; sourceTree = BUILT_PRODUCTS_DIR; };
22 | 45964D362282A1FD00C16658 /* mpp-library */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "mpp-library"; path = "../mpp-library"; sourceTree = ""; };
23 | 459B259323FA5E9B00AAF627 /* LoadingTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LoadingTableViewCell.xib; sourceTree = ""; };
24 | 459B259523FA5F2800AAF627 /* ProductTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProductTableViewCell.xib; sourceTree = ""; };
25 | 45D74FCB22BFDDFD00CAB0C8 /* TestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestViewController.swift; sourceTree = ""; };
26 | 45F47913219463C7003D25FA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
27 | 45F47915219463C7003D25FA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
28 | 45F4791A219463C7003D25FA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
29 | 45F4791B219463C7003D25FA /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
30 | 45F4791C219463C7003D25FA /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
31 | A644D2F1C5377C40A53FCD6A /* Pods-TestProj.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TestProj.release.xcconfig"; path = "Pods/Target Support Files/Pods-TestProj/Pods-TestProj.release.xcconfig"; sourceTree = ""; };
32 | DFBDF7D3559D080FDCA444A6 /* Pods-TestProj.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TestProj.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TestProj/Pods-TestProj.debug.xcconfig"; sourceTree = ""; };
33 | E8D683A7C91DCD56058C7435 /* Pods_TestProj.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TestProj.framework; sourceTree = BUILT_PRODUCTS_DIR; };
34 | /* End PBXFileReference section */
35 |
36 | /* Begin PBXFrameworksBuildPhase section */
37 | 287627FC1F319065007FA12B /* Frameworks */ = {
38 | isa = PBXFrameworksBuildPhase;
39 | buildActionMask = 2147483647;
40 | files = (
41 | 2B70A10DE02726CA8E6981EB /* Pods_TestProj.framework in Frameworks */,
42 | );
43 | runOnlyForDeploymentPostprocessing = 0;
44 | };
45 | /* End PBXFrameworksBuildPhase section */
46 |
47 | /* Begin PBXGroup section */
48 | 0C46713DE6750675C174D0A7 /* Pods */ = {
49 | isa = PBXGroup;
50 | children = (
51 | DFBDF7D3559D080FDCA444A6 /* Pods-TestProj.debug.xcconfig */,
52 | A644D2F1C5377C40A53FCD6A /* Pods-TestProj.release.xcconfig */,
53 | );
54 | name = Pods;
55 | sourceTree = "";
56 | };
57 | 287627F61F319065007FA12B = {
58 | isa = PBXGroup;
59 | children = (
60 | 45964D362282A1FD00C16658 /* mpp-library */,
61 | 45F47910219463C7003D25FA /* src */,
62 | 287628001F319065007FA12B /* Products */,
63 | EE1ABB3E79CE541540D3155F /* Frameworks */,
64 | 0C46713DE6750675C174D0A7 /* Pods */,
65 | );
66 | indentWidth = 4;
67 | sourceTree = "";
68 | tabWidth = 4;
69 | usesTabs = 0;
70 | };
71 | 287628001F319065007FA12B /* Products */ = {
72 | isa = PBXGroup;
73 | children = (
74 | 287627FF1F319065007FA12B /* moko-paging.app */,
75 | );
76 | name = Products;
77 | sourceTree = "";
78 | };
79 | 45F47910219463C7003D25FA /* src */ = {
80 | isa = PBXGroup;
81 | children = (
82 | 45F47911219463C7003D25FA /* Resources */,
83 | 45F4791A219463C7003D25FA /* Assets.xcassets */,
84 | 45F4791B219463C7003D25FA /* AppDelegate.swift */,
85 | 45F4791C219463C7003D25FA /* Info.plist */,
86 | 45D74FCB22BFDDFD00CAB0C8 /* TestViewController.swift */,
87 | );
88 | path = src;
89 | sourceTree = "";
90 | };
91 | 45F47911219463C7003D25FA /* Resources */ = {
92 | isa = PBXGroup;
93 | children = (
94 | 459B259323FA5E9B00AAF627 /* LoadingTableViewCell.xib */,
95 | 45F47912219463C7003D25FA /* LaunchScreen.storyboard */,
96 | 45F47914219463C7003D25FA /* Main.storyboard */,
97 | 459B259523FA5F2800AAF627 /* ProductTableViewCell.xib */,
98 | );
99 | path = Resources;
100 | sourceTree = "";
101 | };
102 | EE1ABB3E79CE541540D3155F /* Frameworks */ = {
103 | isa = PBXGroup;
104 | children = (
105 | E8D683A7C91DCD56058C7435 /* Pods_TestProj.framework */,
106 | );
107 | name = Frameworks;
108 | sourceTree = "";
109 | };
110 | /* End PBXGroup section */
111 |
112 | /* Begin PBXNativeTarget section */
113 | 287627FE1F319065007FA12B /* TestProj */ = {
114 | isa = PBXNativeTarget;
115 | buildConfigurationList = 287628111F319065007FA12B /* Build configuration list for PBXNativeTarget "TestProj" */;
116 | buildPhases = (
117 | DDE4C06D580BF457BEDF8D0A /* [CP] Check Pods Manifest.lock */,
118 | 287627FB1F319065007FA12B /* Sources */,
119 | 287627FC1F319065007FA12B /* Frameworks */,
120 | 287627FD1F319065007FA12B /* Resources */,
121 | BB21689CD10B55912ECAB83F /* [CP] Embed Pods Frameworks */,
122 | );
123 | buildRules = (
124 | );
125 | dependencies = (
126 | );
127 | name = TestProj;
128 | productName = TestProj;
129 | productReference = 287627FF1F319065007FA12B /* moko-paging.app */;
130 | productType = "com.apple.product-type.application";
131 | };
132 | /* End PBXNativeTarget section */
133 |
134 | /* Begin PBXProject section */
135 | 287627F71F319065007FA12B /* Project object */ = {
136 | isa = PBXProject;
137 | attributes = {
138 | LastSwiftUpdateCheck = 0830;
139 | LastUpgradeCheck = 0830;
140 | ORGANIZATIONNAME = "IceRock Development";
141 | TargetAttributes = {
142 | 287627FE1F319065007FA12B = {
143 | CreatedOnToolsVersion = 8.3.3;
144 | LastSwiftMigration = 0940;
145 | };
146 | };
147 | };
148 | buildConfigurationList = 287627FA1F319065007FA12B /* Build configuration list for PBXProject "TestProj" */;
149 | compatibilityVersion = "Xcode 9.3";
150 | developmentRegion = English;
151 | hasScannedForEncodings = 0;
152 | knownRegions = (
153 | English,
154 | Base,
155 | ru,
156 | en,
157 | );
158 | mainGroup = 287627F61F319065007FA12B;
159 | productRefGroup = 287628001F319065007FA12B /* Products */;
160 | projectDirPath = "";
161 | projectRoot = "";
162 | targets = (
163 | 287627FE1F319065007FA12B /* TestProj */,
164 | );
165 | };
166 | /* End PBXProject section */
167 |
168 | /* Begin PBXResourcesBuildPhase section */
169 | 287627FD1F319065007FA12B /* Resources */ = {
170 | isa = PBXResourcesBuildPhase;
171 | buildActionMask = 2147483647;
172 | files = (
173 | 45F4791E219463C7003D25FA /* Main.storyboard in Resources */,
174 | 45F4791D219463C7003D25FA /* LaunchScreen.storyboard in Resources */,
175 | 459B259623FA5F2800AAF627 /* ProductTableViewCell.xib in Resources */,
176 | 459B259423FA5E9B00AAF627 /* LoadingTableViewCell.xib in Resources */,
177 | 45F47921219463C7003D25FA /* Assets.xcassets in Resources */,
178 | );
179 | runOnlyForDeploymentPostprocessing = 0;
180 | };
181 | /* End PBXResourcesBuildPhase section */
182 |
183 | /* Begin PBXShellScriptBuildPhase section */
184 | BB21689CD10B55912ECAB83F /* [CP] Embed Pods Frameworks */ = {
185 | isa = PBXShellScriptBuildPhase;
186 | buildActionMask = 2147483647;
187 | files = (
188 | );
189 | inputFileListPaths = (
190 | );
191 | name = "[CP] Embed Pods Frameworks";
192 | outputFileListPaths = (
193 | );
194 | runOnlyForDeploymentPostprocessing = 0;
195 | shellPath = /bin/sh;
196 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-TestProj/Pods-TestProj-frameworks.sh\"\n";
197 | showEnvVarsInLog = 0;
198 | };
199 | DDE4C06D580BF457BEDF8D0A /* [CP] Check Pods Manifest.lock */ = {
200 | isa = PBXShellScriptBuildPhase;
201 | buildActionMask = 2147483647;
202 | files = (
203 | );
204 | inputFileListPaths = (
205 | );
206 | inputPaths = (
207 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
208 | "${PODS_ROOT}/Manifest.lock",
209 | );
210 | name = "[CP] Check Pods Manifest.lock";
211 | outputFileListPaths = (
212 | );
213 | outputPaths = (
214 | "$(DERIVED_FILE_DIR)/Pods-TestProj-checkManifestLockResult.txt",
215 | );
216 | runOnlyForDeploymentPostprocessing = 0;
217 | shellPath = /bin/sh;
218 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
219 | showEnvVarsInLog = 0;
220 | };
221 | /* End PBXShellScriptBuildPhase section */
222 |
223 | /* Begin PBXSourcesBuildPhase section */
224 | 287627FB1F319065007FA12B /* Sources */ = {
225 | isa = PBXSourcesBuildPhase;
226 | buildActionMask = 2147483647;
227 | files = (
228 | 45D74FCC22BFDDFD00CAB0C8 /* TestViewController.swift in Sources */,
229 | 45F47922219463C7003D25FA /* AppDelegate.swift in Sources */,
230 | );
231 | runOnlyForDeploymentPostprocessing = 0;
232 | };
233 | /* End PBXSourcesBuildPhase section */
234 |
235 | /* Begin PBXVariantGroup section */
236 | 45F47912219463C7003D25FA /* LaunchScreen.storyboard */ = {
237 | isa = PBXVariantGroup;
238 | children = (
239 | 45F47913219463C7003D25FA /* Base */,
240 | );
241 | name = LaunchScreen.storyboard;
242 | sourceTree = "";
243 | };
244 | 45F47914219463C7003D25FA /* Main.storyboard */ = {
245 | isa = PBXVariantGroup;
246 | children = (
247 | 45F47915219463C7003D25FA /* Base */,
248 | );
249 | name = Main.storyboard;
250 | sourceTree = "";
251 | };
252 | /* End PBXVariantGroup section */
253 |
254 | /* Begin XCBuildConfiguration section */
255 | 2876280F1F319065007FA12B /* Debug */ = {
256 | isa = XCBuildConfiguration;
257 | buildSettings = {
258 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
259 | CURRENT_PROJECT_VERSION = 0;
260 | DEFINES_MODULE = YES;
261 | ONLY_ACTIVE_ARCH = YES;
262 | SWIFT_VERSION = 4.0;
263 | };
264 | name = Debug;
265 | };
266 | 287628101F319065007FA12B /* Release */ = {
267 | isa = XCBuildConfiguration;
268 | buildSettings = {
269 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
270 | CURRENT_PROJECT_VERSION = 0;
271 | DEFINES_MODULE = YES;
272 | ONLY_ACTIVE_ARCH = YES;
273 | SWIFT_VERSION = 4.0;
274 | };
275 | name = Release;
276 | };
277 | 287628121F319065007FA12B /* Debug */ = {
278 | isa = XCBuildConfiguration;
279 | baseConfigurationReference = DFBDF7D3559D080FDCA444A6 /* Pods-TestProj.debug.xcconfig */;
280 | buildSettings = {
281 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
282 | CODE_SIGN_IDENTITY = "iPhone Developer";
283 | CODE_SIGN_STYLE = Automatic;
284 | DEVELOPMENT_TEAM = 4VU932NX78;
285 | INFOPLIST_FILE = src/Info.plist;
286 | PRODUCT_BUNDLE_IDENTIFIER = dev.icerock.moko.sample.paging;
287 | PRODUCT_NAME = "moko-paging";
288 | PROVISIONING_PROFILE_SPECIFIER = "";
289 | SDKROOT = iphoneos;
290 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
291 | SWIFT_SWIFT3_OBJC_INFERENCE = On;
292 | };
293 | name = Debug;
294 | };
295 | 287628131F319065007FA12B /* Release */ = {
296 | isa = XCBuildConfiguration;
297 | baseConfigurationReference = A644D2F1C5377C40A53FCD6A /* Pods-TestProj.release.xcconfig */;
298 | buildSettings = {
299 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
300 | CODE_SIGN_IDENTITY = "iPhone Developer";
301 | CODE_SIGN_STYLE = Automatic;
302 | DEVELOPMENT_TEAM = 4VU932NX78;
303 | INFOPLIST_FILE = src/Info.plist;
304 | PRODUCT_BUNDLE_IDENTIFIER = dev.icerock.moko.sample.paging;
305 | PRODUCT_NAME = "moko-paging";
306 | PROVISIONING_PROFILE_SPECIFIER = "";
307 | SDKROOT = iphoneos;
308 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
309 | SWIFT_SWIFT3_OBJC_INFERENCE = On;
310 | };
311 | name = Release;
312 | };
313 | /* End XCBuildConfiguration section */
314 |
315 | /* Begin XCConfigurationList section */
316 | 287627FA1F319065007FA12B /* Build configuration list for PBXProject "TestProj" */ = {
317 | isa = XCConfigurationList;
318 | buildConfigurations = (
319 | 2876280F1F319065007FA12B /* Debug */,
320 | 287628101F319065007FA12B /* Release */,
321 | );
322 | defaultConfigurationIsVisible = 0;
323 | defaultConfigurationName = Release;
324 | };
325 | 287628111F319065007FA12B /* Build configuration list for PBXNativeTarget "TestProj" */ = {
326 | isa = XCConfigurationList;
327 | buildConfigurations = (
328 | 287628121F319065007FA12B /* Debug */,
329 | 287628131F319065007FA12B /* Release */,
330 | );
331 | defaultConfigurationIsVisible = 0;
332 | defaultConfigurationName = Release;
333 | };
334 | /* End XCConfigurationList section */
335 | };
336 | rootObject = 287627F71F319065007FA12B /* Project object */;
337 | }
338 |
--------------------------------------------------------------------------------