diff --git a/.idea/copyright/IceRock.xml b/.idea/copyright/IceRock.xml
new file mode 100644
index 0000000..0aa66dc
--- /dev/null
+++ b/.idea/copyright/IceRock.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
new file mode 100644
index 0000000..98ac983
--- /dev/null
+++ b/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index d13e8a3..fb5159d 100755
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ This is a Kotlin MultiPlatform library that contains pagination logic for kotlin
- **Pagination** implements pagination logic for the data from abstract `PagedListDataSource`.
- Managing a data loading process using **Pagination** asynchronous functions: `loadFirstPage`, `loadNextPage`,
`refresh` or their duplicates with `suspend` modifier.
-- Observing states of **Pagination** using `LiveData` from **moko-mvvm**.
+- Observing states of **Pagination** using `LiveData` from **moko-mvvm** or `Flow` from **kotlinx.coroutines**.
## Requirements
- Gradle version 6.8+
@@ -38,12 +38,18 @@ allprojects {
project build.gradle
```groovy
dependencies {
- commonMainApi("dev.icerock.moko:paging:0.7.1")
+ commonMainApi("dev.icerock.moko:paging-core:0.8.0")
+ commonMainApi("dev.icerock.moko:paging-livedata:0.8.0")
+ commonMainApi("dev.icerock.moko:paging-flow:0.8.0")
+ commonMainApi("dev.icerock.moko:paging-state:0.8.0")
+ commonMainApi("dev.icerock.moko:paging-test:0.8.0")
}
```
## Usage
+### LiveData
+
You can use **Pagination** in `commonMain` sourceset.
**Pagination** creation:
@@ -110,11 +116,35 @@ pagination.refreshLoading.addObserver { isRefreshing: Boolean ->
}
```
+### Flow
+
+```kotlin
+// Observing the state of the pagination
+pagination.state
+ .onEach { /* ... */ }
+ .launchIn(coroutineScope)
+
+// Observing the next page loading process
+pagination.nextPageLoading
+ .onEach { /* ... */ }
+ .launchIn(coroutineScope)
+
+// Observing the refresh process
+pagination.refreshLoading
+ .onEach { /* ... */ }
+ .launchIn(coroutineScope)
+```
+
+
## Samples
-Please see more examples in the [sample directory](sample).
+Please see more examples in the [sample directory](sample) or [sample-declarative-ui](sample-declarative-ui)
## Set Up Locally
-- The [paging directory](paging) contains the `paging` library;
+- The [paging-core directory](paging-core) contains the core - pagination logic & data sources;
+- The [paging-livedata directory](paging-livedata) contains implementation of the Pagination using `moko-mvvv-livedata`;
+- The [paging-flow directory](paging-flow) contains implementation of the Pagination using `kotlinx.coroutines`;
+- The [paging-state directory](paging-state) contains a set of code for working with the state of resources;
+- The [paging-test directory](paging-test) contains a set of tests for pagination;
- The [sample directory](sample) contains sample apps for Android and iOS; plus the mpp-library connected to the apps.
## Contributing
diff --git a/build.gradle.kts b/build.gradle.kts
index 0969237..cac674c 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -18,6 +18,7 @@ buildscript {
classpath(libs.mobileMultiplatformGradlePlugin)
classpath(libs.kotlinSerializationGradlePlugin)
classpath(libs.mokoUnitsGeneratorGradlePlugin)
+ classpath(libs.mokoKSwiftGradle)
}
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 1583b62..292915d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -7,10 +7,11 @@ recyclerViewVersion = "1.1.0"
swipeRefreshLayoutVersion = "1.1.0"
ktorClientVersion = "2.0.0"
coroutinesVersion = "1.6.0-native-mt"
-mokoMvvmVersion = "0.12.0"
-mokoResourcesVersion = "0.18.0"
+mokoMvvmVersion = "0.13.0"
+mokoResourcesVersion = "0.20.1"
mokoUnitsVersion = "0.8.0"
-mokoPagingVersion = "0.7.1"
+mokoKSwiftVersion = "0.5.0"
+mokoPagingVersion = "0.8.0"
[libraries]
appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" }
@@ -24,6 +25,9 @@ mokoUnits = { module = "dev.icerock.moko:units", version.ref = "mokoUnitsVersion
mokoUnitsDataBinding = { module = "dev.icerock.moko:units-databinding", version.ref = "mokoUnitsVersion" }
mokoMvvmLiveData = { module = "dev.icerock.moko:mvvm-livedata", version.ref = "mokoMvvmVersion" }
mokoMvvmState = { module = "dev.icerock.moko:mvvm-state", version.ref = "mokoMvvmVersion" }
+mokoMvvmFlow = { module = "dev.icerock.moko:mvvm-flow", version.ref = "mokoMvvmVersion" }
+mokoMvvmCore = { module = "dev.icerock.moko:mvvm-core", version.ref = "mokoMvvmVersion" }
+mokoMvvmFlowCompose = { module = "dev.icerock.moko:mvvm-flow-compose", version.ref = "mokoMvvmVersion" }
kotlinTestJUnit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlinVersion" }
androidCoreTesting = { module = "androidx.arch.core:core-testing", version.ref = "androidCoreTestingVersion" }
ktorClientMock = { module = "io.ktor:ktor-client-mock", version.ref = "ktorClientVersion" }
@@ -36,3 +40,5 @@ mokoGradlePlugin = { module = "dev.icerock.moko:moko-gradle-plugin", version = "
mobileMultiplatformGradlePlugin = { module = "dev.icerock:mobile-multiplatform", version = "0.14.1" }
kotlinSerializationGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlinVersion" }
mokoUnitsGeneratorGradlePlugin = { module = "dev.icerock.moko:units-generator", version.ref = "mokoUnitsVersion" }
+
+mokoKSwiftGradle = { module = "dev.icerock.moko:kswift-gradle-plugin", version.ref = "mokoKSwiftVersion" }
diff --git a/paging-core/build.gradle.kts b/paging-core/build.gradle.kts
new file mode 100644
index 0000000..9bc001b
--- /dev/null
+++ b/paging-core/build.gradle.kts
@@ -0,0 +1,14 @@
+/*
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+plugins {
+ id("dev.icerock.moko.gradle.multiplatform.mobile")
+ id("dev.icerock.moko.gradle.publication")
+ id("dev.icerock.moko.gradle.stub.javadoc")
+ id("dev.icerock.moko.gradle.detekt")
+}
+
+dependencies {
+ commonMainApi(libs.coroutines)
+}
diff --git a/paging-core/src/androidMain/AndroidManifest.xml b/paging-core/src/androidMain/AndroidManifest.xml
new file mode 100755
index 0000000..65a4790
--- /dev/null
+++ b/paging-core/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/PagingDeprecated.kt b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/PagingDeprecated.kt
new file mode 100644
index 0000000..c464693
--- /dev/null
+++ b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/PagingDeprecated.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package dev.icerock.moko.paging
+
+@Deprecated(
+ message = "deprecated due to package renaming",
+ replaceWith = ReplaceWith("IdEntity", "dev.icerock.moko.paging.core"),
+ level = DeprecationLevel.WARNING
+)
+typealias IdEntity = dev.icerock.moko.paging.core.IdEntity
+
+@Deprecated(
+ message = "deprecated due to package renaming",
+ replaceWith = ReplaceWith("IdComparator", "dev.icerock.moko.paging.core"),
+ level = DeprecationLevel.WARNING
+)
+typealias IdComparator = dev.icerock.moko.paging.core.IdComparator
+
+@Deprecated(
+ message = "deprecated due to package renaming",
+ replaceWith = ReplaceWith("LambdaPagedListDataSource", "dev.icerock.moko.paging.core"),
+ level = DeprecationLevel.WARNING
+)
+typealias LambdaPagedListDataSource = dev.icerock.moko.paging.core.LambdaPagedListDataSource
+
+@Deprecated(
+ message = "deprecated due to package renaming",
+ replaceWith = ReplaceWith("PagedListDataSource", "dev.icerock.moko.paging.core"),
+ level = DeprecationLevel.WARNING
+)
+typealias PagedListDataSource = dev.icerock.moko.paging.core.PagedListDataSource
+
+@Deprecated(
+ message = "deprecated due to package renaming",
+ replaceWith = ReplaceWith("ReachEndNotifierList", "dev.icerock.moko.paging.core"),
+ level = DeprecationLevel.WARNING
+)
+typealias ReachEndNotifierList = dev.icerock.moko.paging.core.ReachEndNotifierList
diff --git a/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Identify.kt b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Identify.kt
new file mode 100644
index 0000000..2201ea5
--- /dev/null
+++ b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Identify.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package dev.icerock.moko.paging.core
+
+interface IdEntity {
+ val id: Long
+}
+
+class IdComparator : Comparator {
+ override fun compare(a: T, b: T): Int {
+ return if (a.id == b.id) 0 else 1
+ }
+}
diff --git a/paging/src/commonMain/kotlin/dev/icerock/moko/paging/LambdaPagedListDataSource.kt b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/LambdaPagedListDataSource.kt
similarity index 90%
rename from paging/src/commonMain/kotlin/dev/icerock/moko/paging/LambdaPagedListDataSource.kt
rename to paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/LambdaPagedListDataSource.kt
index 17d0de3..2a1142c 100644
--- a/paging/src/commonMain/kotlin/dev/icerock/moko/paging/LambdaPagedListDataSource.kt
+++ b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/LambdaPagedListDataSource.kt
@@ -2,7 +2,7 @@
* Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/
-package dev.icerock.moko.paging
+package dev.icerock.moko.paging.core
class LambdaPagedListDataSource(
private val loadPageLambda: suspend (List?) -> List
diff --git a/paging/src/commonMain/kotlin/dev/icerock/moko/paging/PagedListDataSource.kt b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/PagedListDataSource.kt
similarity index 84%
rename from paging/src/commonMain/kotlin/dev/icerock/moko/paging/PagedListDataSource.kt
rename to paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/PagedListDataSource.kt
index bbff503..141feac 100644
--- a/paging/src/commonMain/kotlin/dev/icerock/moko/paging/PagedListDataSource.kt
+++ b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/PagedListDataSource.kt
@@ -2,7 +2,7 @@
* Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/
-package dev.icerock.moko.paging
+package dev.icerock.moko.paging.core
interface PagedListDataSource {
suspend fun loadPage(currentList: List?): List
diff --git a/paging/src/commonMain/kotlin/dev/icerock/moko/paging/Pagination.kt b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Pagination.kt
similarity index 68%
rename from paging/src/commonMain/kotlin/dev/icerock/moko/paging/Pagination.kt
rename to paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Pagination.kt
index f596fe3..6d9bbf0 100644
--- a/paging/src/commonMain/kotlin/dev/icerock/moko/paging/Pagination.kt
+++ b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Pagination.kt
@@ -1,54 +1,58 @@
/*
- * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/
-package dev.icerock.moko.paging
+package dev.icerock.moko.paging.core
-import dev.icerock.moko.mvvm.ResourceState
-import dev.icerock.moko.mvvm.asState
-import dev.icerock.moko.mvvm.livedata.MutableLiveData
-import dev.icerock.moko.mvvm.livedata.readOnly
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlin.coroutines.CoroutineContext
-class Pagination- (
+@Suppress("TooManyFunctions")
+abstract class Pagination
- (
parentScope: CoroutineScope,
private val dataSource: PagedListDataSource
- ,
private val comparator: Comparator
- ,
private val nextPageListener: (Result
>) -> Unit,
private val refreshListener: (Result>) -> Unit,
- initValue: List- ? = null
) : CoroutineScope {
override val coroutineContext: CoroutineContext = parentScope.coroutineContext
- private val mStateStorage =
- MutableLiveData, Throwable>>(initValue.asStateNullIsLoading())
+ protected val mNextPageLoading: MutableStateFlow = MutableStateFlow(false)
+ protected val mRefreshLoading: MutableStateFlow = MutableStateFlow(false)
+ private val mEndOfList: MutableStateFlow = MutableStateFlow(false)
- val state = mStateStorage.readOnly()
-
- private val mNextPageLoading = MutableLiveData(false)
- val nextPageLoading = mNextPageLoading.readOnly()
-
- private val mEndOfList = MutableLiveData(false)
+ private var loadNextPageDeferred: Deferred
>? = null
+ private val listMutex = Mutex()
- private val mRefreshLoading = MutableLiveData(false)
- val refreshLoading = mRefreshLoading.readOnly()
+ fun loadFirstPage() {
+ launch { loadFirstPageSuspend() }
+ }
- private val listMutex = Mutex()
+ fun loadNextPage() {
+ launch { loadNextPageSuspend() }
+ }
- private var loadNextPageDeferred: Deferred>? = null
+ fun refresh() {
+ launch { refreshSuspend() }
+ }
- fun loadFirstPage() {
- launch {
- loadFirstPageSuspend()
- }
+ fun setData(items: List- ?) {
+ launch { setDataSuspend(items) }
}
+ abstract fun dataValue(): List
- ?
+ protected abstract fun saveState(items: List
- )
+ protected abstract fun saveStateNullIsEmpty(items: List
- ?)
+ protected abstract fun saveStateNullIsLoading(items: List
- ?)
+ protected abstract fun saveErrorState(error: Exception)
+ protected abstract fun setupLoadingState()
+
suspend fun loadFirstPageSuspend() {
loadNextPageDeferred?.cancel()
@@ -56,24 +60,18 @@ class Pagination
- (
mEndOfList.value = false
mNextPageLoading.value = false
- mStateStorage.value = ResourceState.Loading()
+ setupLoadingState()
@Suppress("TooGenericExceptionCaught")
try {
val items: List
- = dataSource.loadPage(null)
- mStateStorage.value = items.asState()
+ saveState(items)
} catch (error: Exception) {
- mStateStorage.value = ResourceState.Failed(error)
+ saveErrorState(error)
}
listMutex.unlock()
}
- fun loadNextPage() {
- launch {
- loadNextPageSuspend()
- }
- }
-
@Suppress("ReturnCount")
suspend fun loadNextPageSuspend() {
if (mNextPageLoading.value) return
@@ -87,7 +85,7 @@ class Pagination
- (
@Suppress("TooGenericExceptionCaught")
try {
loadNextPageDeferred = this.async {
- val currentList = mStateStorage.value.dataValue()
+ val currentList = dataValue()
?: throw IllegalStateException("Try to load next page when list is empty")
// load next page items
val items = dataSource.loadPage(currentList)
@@ -103,7 +101,7 @@ class Pagination
- (
mEndOfList.value = true
} else {
// save
- mStateStorage.value = newList.asState()
+ saveState(newList)
}
newList
}
@@ -122,12 +120,6 @@ class Pagination
- (
listMutex.unlock()
}
- fun refresh() {
- launch {
- refreshSuspend()
- }
- }
-
suspend fun refreshSuspend() {
loadNextPageDeferred?.cancel()
listMutex.lock()
@@ -148,7 +140,7 @@ class Pagination
- (
// load first page items
val items = dataSource.loadPage(null)
// save
- mStateStorage.value = items.asState()
+ saveState(items)
// flag
mEndOfList.value = false
mRefreshLoading.value = false
@@ -163,34 +155,10 @@ class Pagination
- (
listMutex.unlock()
}
- fun setData(items: List
- ?) {
- launch {
- setDataSuspend(items)
- }
- }
-
suspend fun setDataSuspend(items: List
- ?) {
listMutex.lock()
- mStateStorage.value = items.asStateNullIsEmpty()
+ saveStateNullIsEmpty(items)
mEndOfList.value = false
listMutex.unlock()
}
}
-
-fun List?.asStateNullIsEmpty() = asState {
- ResourceState.Empty
, E>()
-}
-
-fun List?.asStateNullIsLoading() = asState {
- ResourceState.Loading, E>()
-}
-
-interface IdEntity {
- val id: Long
-}
-
-class IdComparator : Comparator {
- override fun compare(a: T, b: T): Int {
- return if (a.id == b.id) 0 else 1
- }
-}
diff --git a/paging/src/commonMain/kotlin/dev/icerock/moko/paging/ReachEndNotifierList.kt b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/ReachEndNotifierList.kt
similarity index 97%
rename from paging/src/commonMain/kotlin/dev/icerock/moko/paging/ReachEndNotifierList.kt
rename to paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/ReachEndNotifierList.kt
index 96e7ed4..05d2170 100644
--- a/paging/src/commonMain/kotlin/dev/icerock/moko/paging/ReachEndNotifierList.kt
+++ b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/ReachEndNotifierList.kt
@@ -2,7 +2,7 @@
* Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/
-package dev.icerock.moko.paging
+package dev.icerock.moko.paging.core
@Suppress("TooManyFunctions")
class ReachEndNotifierList(
diff --git a/paging-flow/build.gradle.kts b/paging-flow/build.gradle.kts
new file mode 100644
index 0000000..6afed04
--- /dev/null
+++ b/paging-flow/build.gradle.kts
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+plugins {
+ id("dev.icerock.moko.gradle.multiplatform.mobile")
+ id("dev.icerock.moko.gradle.publication")
+ id("dev.icerock.moko.gradle.stub.javadoc")
+ id("dev.icerock.moko.gradle.detekt")
+}
+
+dependencies {
+ commonMainApi(libs.mokoMvvmFlow)
+ commonMainApi(libs.mokoMvvmCore)
+ commonMainApi(projects.pagingCore)
+ commonMainApi(projects.pagingState)
+
+ commonTestImplementation(projects.pagingTest)
+}
diff --git a/paging-flow/src/androidMain/AndroidManifest.xml b/paging-flow/src/androidMain/AndroidManifest.xml
new file mode 100755
index 0000000..53dd8c7
--- /dev/null
+++ b/paging-flow/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/paging-flow/src/commonMain/kotlin/dev/icerock/moko/paging/flow/FlowExt.kt b/paging-flow/src/commonMain/kotlin/dev/icerock/moko/paging/flow/FlowExt.kt
new file mode 100644
index 0000000..340b95f
--- /dev/null
+++ b/paging-flow/src/commonMain/kotlin/dev/icerock/moko/paging/flow/FlowExt.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package dev.icerock.moko.paging.flow
+
+import dev.icerock.moko.paging.core.ReachEndNotifierList
+import dev.icerock.moko.paging.core.withReachEndNotifier
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+fun Flow>.withLoadingItem(
+ loading: Flow,
+ itemFactory: () -> T
+): Flow> = combine(this, loading) { items, nextPageLoading ->
+ if (nextPageLoading) {
+ items + itemFactory()
+ } else {
+ items
+ }
+}
+
+fun Flow>.withReachEndNotifier(
+ action: (Int) -> Unit
+): Flow> = map { list ->
+ list.withReachEndNotifier(action)
+}
+
+fun Flow>.stateWithLoadingItem(
+ parentScope: CoroutineScope,
+ loading: Flow,
+ itemFactory: () -> T
+): StateFlow> = this.withLoadingItem(
+ loading = loading,
+ itemFactory = itemFactory
+).stateIn(parentScope, SharingStarted.Lazily, emptyList())
+
+fun Flow>.stateWithReachEndNotifier(
+ parentScope: CoroutineScope,
+ action: (Int) -> Unit
+): StateFlow> =
+ this.withReachEndNotifier(action)
+ .stateIn(
+ parentScope,
+ SharingStarted.Lazily,
+ ReachEndNotifierList(emptyList(), action)
+ )
diff --git a/paging-flow/src/commonMain/kotlin/dev/icerock/moko/paging/flow/Pagination.kt b/paging-flow/src/commonMain/kotlin/dev/icerock/moko/paging/flow/Pagination.kt
new file mode 100644
index 0000000..fb5b17e
--- /dev/null
+++ b/paging-flow/src/commonMain/kotlin/dev/icerock/moko/paging/flow/Pagination.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package dev.icerock.moko.paging.flow
+
+import dev.icerock.moko.mvvm.flow.CStateFlow
+import dev.icerock.moko.mvvm.flow.cStateFlow
+import dev.icerock.moko.paging.core.PagedListDataSource
+import dev.icerock.moko.paging.core.Pagination
+import dev.icerock.moko.paging.state.ResourceState
+import dev.icerock.moko.paging.state.asState
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+
+typealias PaginationState- = ResourceState
, Throwable>
+
+class Pagination- (
+ parentScope: CoroutineScope,
+ dataSource: PagedListDataSource
- ,
+ comparator: Comparator
- ,
+ nextPageListener: (Result
>) -> Unit,
+ refreshListener: (Result>) -> Unit,
+ initValue: List- ? = null
+) : Pagination
- (
+ parentScope = parentScope,
+ dataSource = dataSource,
+ comparator = comparator,
+ nextPageListener = nextPageListener,
+ refreshListener = refreshListener,
+) {
+
+ private val _state: MutableStateFlow> =
+ MutableStateFlow(initValue.asStateNullIsLoading())
+
+ val state: CStateFlow, Throwable>>
+ get() = _state.cStateFlow()
+
+ val refreshLoading: CStateFlow
+ get() = mRefreshLoading.cStateFlow()
+
+ val nextPageLoading: CStateFlow
+ get() = mNextPageLoading.cStateFlow()
+
+ override fun dataValue(): List
- ? =
+ _state.value.dataValue()
+
+ override fun saveState(items: List
- ) {
+ _state.value = items.asState()
+ }
+
+ override fun saveStateNullIsEmpty(items: List
- ?) {
+ _state.value = items.asStateNullIsEmpty()
+ }
+
+ override fun saveStateNullIsLoading(items: List
- ?) {
+ _state.value = items.asStateNullIsLoading()
+ }
+
+ override fun saveErrorState(error: Exception) {
+ _state.value = error.asState()
+ }
+
+ override fun setupLoadingState() {
+ _state.value = ResourceState.Loading
+ }
+}
+
+fun List?.asStateNullIsEmpty(): ResourceState
, E> = asState {
+ ResourceState.Empty
+}
+
+fun List?.asStateNullIsLoading(): ResourceState, E> = asState {
+ ResourceState.Loading
+}
diff --git a/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/IntegrationFlowTests.kt b/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/IntegrationFlowTests.kt
new file mode 100644
index 0000000..b7abff9
--- /dev/null
+++ b/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/IntegrationFlowTests.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package dev.icerock.moko.paging.flow
+
+import dev.icerock.moko.paging.core.PagedListDataSource
+import dev.icerock.moko.paging.test.IntegrationTests
+import kotlinx.coroutines.CoroutineScope
+import kotlin.test.Test
+
+class IntegrationFlowTests: IntegrationTests() {
+
+ override fun createPagination(
+ parentScope: CoroutineScope,
+ dataSource: PagedListDataSource,
+ comparator: Comparator,
+ nextPageListener: (Result>) -> Unit,
+ refreshListener: (Result>) -> Unit
+ ): Pagination {
+ return Pagination(
+ parentScope = parentScope,
+ dataSource = dataSource,
+ comparator = comparator,
+ nextPageListener = nextPageListener,
+ refreshListener = refreshListener
+ )
+ }
+
+ @Test
+ override fun parallelRequests() = super.parallelRequests()
+
+ @Test
+ override fun parallelRequestsAndSetData() = super.parallelRequestsAndSetData()
+
+ @Test
+ override fun closingScope() = super.closingScope()
+}
diff --git a/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/PaginationFlowTest.kt b/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/PaginationFlowTest.kt
new file mode 100644
index 0000000..af79e75
--- /dev/null
+++ b/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/PaginationFlowTest.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package dev.icerock.moko.paging.flow
+
+import dev.icerock.moko.paging.core.PagedListDataSource
+import dev.icerock.moko.paging.state.ResourceState
+import dev.icerock.moko.paging.test.PaginationTest
+import kotlinx.coroutines.CoroutineScope
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+
+class PaginationFlowTest: PaginationTest() {
+
+ override fun createPagination(
+ parentScope: CoroutineScope,
+ dataSource: PagedListDataSource,
+ comparator: Comparator,
+ nextPageListener: (Result>) -> Unit,
+ refreshListener: (Result>) -> Unit
+ ): Pagination {
+ return Pagination(
+ parentScope = parentScope,
+ dataSource = dataSource,
+ comparator = comparator,
+ nextPageListener = nextPageListener,
+ refreshListener = refreshListener
+ )
+ }
+
+ override fun isSuccessState(pagination: dev.icerock.moko.paging.core.Pagination): Boolean {
+ return (pagination as Pagination).state.value is ResourceState.Data<*>
+ }
+
+ @BeforeTest
+ override fun setup() = super.setup()
+
+ @Test
+ override fun `load first page`() = super.`load first page`()
+
+ @Test
+ override fun `load next page`() = super.`load next page`()
+
+ @Test
+ override fun `refresh pagination`() = super.`refresh pagination`()
+
+ @Test
+ override fun `set data`() = super.`set data`()
+
+ @Test
+ override fun `double refresh`() = super.`double refresh`()
+}
diff --git a/paging/build.gradle.kts b/paging-livedata/build.gradle.kts
similarity index 56%
rename from paging/build.gradle.kts
rename to paging-livedata/build.gradle.kts
index bb34262..f04e3b9 100644
--- a/paging/build.gradle.kts
+++ b/paging-livedata/build.gradle.kts
@@ -9,18 +9,10 @@ plugins {
id("dev.icerock.moko.gradle.detekt")
}
-kotlin {
- jvm()
-}
-
dependencies {
- commonMainImplementation(libs.coroutines)
commonMainApi(libs.mokoMvvmLiveData)
commonMainApi(libs.mokoMvvmState)
+ commonMainApi(projects.pagingCore)
- commonTestImplementation(libs.kotlinTestJUnit)
- androidTestImplementation(libs.androidCoreTesting)
- commonTestImplementation(libs.ktorClient)
- commonTestImplementation(libs.ktorClientMock)
- iosX64TestImplementation(libs.coroutines)
+ commonTestImplementation(projects.pagingTest)
}
diff --git a/paging-livedata/src/androidMain/AndroidManifest.xml b/paging-livedata/src/androidMain/AndroidManifest.xml
new file mode 100755
index 0000000..c6f1dbb
--- /dev/null
+++ b/paging-livedata/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/PaginationDeprecated.kt b/paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/PaginationDeprecated.kt
new file mode 100644
index 0000000..d86f177
--- /dev/null
+++ b/paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/PaginationDeprecated.kt
@@ -0,0 +1,13 @@
+/*
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+@file:Suppress("Filename")
+
+package dev.icerock.moko.paging
+
+@Deprecated(
+ message = "deprecated due to package renaming",
+ replaceWith = ReplaceWith("Pagination", "dev.icerock.moko.paging.livedata"),
+ level = DeprecationLevel.WARNING
+)
+typealias Pagination- = dev.icerock.moko.paging.livedata.Pagination
-
diff --git a/paging/src/commonMain/kotlin/dev/icerock/moko/paging/LiveDataExt.kt b/paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/livedata/LiveDataExt.kt
similarity index 73%
rename from paging/src/commonMain/kotlin/dev/icerock/moko/paging/LiveDataExt.kt
rename to paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/livedata/LiveDataExt.kt
index 42e8664..3260cdc 100644
--- a/paging/src/commonMain/kotlin/dev/icerock/moko/paging/LiveDataExt.kt
+++ b/paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/livedata/LiveDataExt.kt
@@ -1,12 +1,14 @@
/*
- * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/
-package dev.icerock.moko.paging
+package dev.icerock.moko.paging.livedata
import dev.icerock.moko.mvvm.livedata.LiveData
import dev.icerock.moko.mvvm.livedata.map
import dev.icerock.moko.mvvm.livedata.mediatorOf
+import dev.icerock.moko.paging.core.ReachEndNotifierList
+import dev.icerock.moko.paging.core.withReachEndNotifier
fun LiveData
>.withLoadingItem(
loading: LiveData,
diff --git a/paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/livedata/Pagination.kt b/paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/livedata/Pagination.kt
new file mode 100644
index 0000000..dc3a122
--- /dev/null
+++ b/paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/livedata/Pagination.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package dev.icerock.moko.paging.livedata
+
+import dev.icerock.moko.mvvm.ResourceState
+import dev.icerock.moko.mvvm.asState
+import dev.icerock.moko.mvvm.livedata.LiveData
+import dev.icerock.moko.mvvm.livedata.MutableLiveData
+import dev.icerock.moko.mvvm.livedata.asLiveData
+import dev.icerock.moko.mvvm.livedata.readOnly
+import dev.icerock.moko.paging.core.PagedListDataSource
+import dev.icerock.moko.paging.core.Pagination
+import kotlinx.coroutines.CoroutineScope
+
+class Pagination- (
+ private val parentScope: CoroutineScope,
+ dataSource: PagedListDataSource
- ,
+ comparator: Comparator
- ,
+ nextPageListener: (Result
>) -> Unit,
+ refreshListener: (Result>) -> Unit,
+ initValue: List- ? = null
+) : Pagination
- (
+ parentScope = parentScope,
+ dataSource = dataSource,
+ comparator = comparator,
+ nextPageListener = nextPageListener,
+ refreshListener = refreshListener,
+) {
+
+ private val _state: MutableLiveData, Throwable>> =
+ MutableLiveData(initValue.asStateNullIsLoading())
+
+ val state: LiveData, Throwable>>
+ get() = _state.readOnly()
+
+ val refreshLoading: LiveData
+ get() = mRefreshLoading.asLiveData(parentScope)
+
+ val nextPageLoading: LiveData
+ get() = mNextPageLoading.asLiveData(parentScope)
+
+ override fun dataValue(): List
- ? =
+ _state.value.dataValue()
+
+ override fun saveState(items: List
- ) {
+ _state.value = items.asState()
+ }
+
+ override fun saveStateNullIsEmpty(items: List
- ?) {
+ _state.value = items.asStateNullIsEmpty()
+ }
+
+ override fun saveStateNullIsLoading(items: List
- ?) {
+ _state.value = items.asStateNullIsLoading()
+ }
+
+ override fun saveErrorState(error: Exception) {
+ _state.value = ResourceState.Failed(error)
+ }
+
+ override fun setupLoadingState() {
+ _state.value = ResourceState.Loading()
+ }
+}
+
+fun List?.asStateNullIsEmpty() = asState {
+ ResourceState.Empty
, E>()
+}
+
+fun List?.asStateNullIsLoading() = asState {
+ ResourceState.Loading, E>()
+}
diff --git a/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/IntegrationLiveTests.kt b/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/IntegrationLiveTests.kt
new file mode 100644
index 0000000..f5cb3ca
--- /dev/null
+++ b/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/IntegrationLiveTests.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package dev.icerock.moko.paging.livedata
+
+import dev.icerock.moko.paging.core.PagedListDataSource
+import dev.icerock.moko.paging.test.IntegrationTests
+import kotlinx.coroutines.CoroutineScope
+import kotlin.test.Test
+
+class IntegrationLiveTests: IntegrationTests() {
+
+ override fun createPagination(
+ parentScope: CoroutineScope,
+ dataSource: PagedListDataSource,
+ comparator: Comparator,
+ nextPageListener: (Result>) -> Unit,
+ refreshListener: (Result>) -> Unit
+ ): Pagination {
+ return Pagination(
+ parentScope = parentScope,
+ dataSource = dataSource,
+ comparator = comparator,
+ nextPageListener = nextPageListener,
+ refreshListener = refreshListener
+ )
+ }
+
+ @Test
+ override fun parallelRequests() = super.parallelRequests()
+
+ @Test
+ override fun parallelRequestsAndSetData() = super.parallelRequestsAndSetData()
+
+ @Test
+ override fun closingScope() = super.closingScope()
+}
diff --git a/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/PaginationLiveTest.kt b/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/PaginationLiveTest.kt
new file mode 100644
index 0000000..6d14692
--- /dev/null
+++ b/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/PaginationLiveTest.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package dev.icerock.moko.paging.livedata
+
+import dev.icerock.moko.paging.core.PagedListDataSource
+import dev.icerock.moko.paging.test.PaginationTest
+import kotlinx.coroutines.CoroutineScope
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+
+class PaginationFlowTest: PaginationTest() {
+
+ override fun createPagination(
+ parentScope: CoroutineScope,
+ dataSource: PagedListDataSource,
+ comparator: Comparator,
+ nextPageListener: (Result>) -> Unit,
+ refreshListener: (Result>) -> Unit
+ ): Pagination {
+ return Pagination(
+ parentScope = parentScope,
+ dataSource = dataSource,
+ comparator = comparator,
+ nextPageListener = nextPageListener,
+ refreshListener = refreshListener
+ )
+ }
+
+ override fun isSuccessState(pagination: dev.icerock.moko.paging.core.Pagination): Boolean {
+ return (pagination as Pagination).state.value.isSuccess()
+ }
+
+ @BeforeTest
+ override fun setup() = super.setup()
+
+ @Test
+ override fun `load first page`() = super.`load first page`()
+
+ @Test
+ override fun `load next page`() = super.`load next page`()
+
+ @Test
+ override fun `refresh pagination`() = super.`refresh pagination`()
+
+ @Test
+ override fun `set data`() = super.`set data`()
+
+ @Test
+ override fun `double refresh`() = super.`double refresh`()
+}
diff --git a/paging-state/build.gradle.kts b/paging-state/build.gradle.kts
new file mode 100644
index 0000000..3e876b2
--- /dev/null
+++ b/paging-state/build.gradle.kts
@@ -0,0 +1,10 @@
+/*
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+plugins {
+ id("dev.icerock.moko.gradle.multiplatform.mobile")
+ id("dev.icerock.moko.gradle.publication")
+ id("dev.icerock.moko.gradle.stub.javadoc")
+ id("dev.icerock.moko.gradle.detekt")
+}
diff --git a/paging-state/src/androidMain/AndroidManifest.xml b/paging-state/src/androidMain/AndroidManifest.xml
new file mode 100755
index 0000000..6e3b6b0
--- /dev/null
+++ b/paging-state/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/paging-state/src/commonMain/kotlin/dev/icerock/moko/paging/state/ResourceState.kt b/paging-state/src/commonMain/kotlin/dev/icerock/moko/paging/state/ResourceState.kt
new file mode 100644
index 0000000..48e4747
--- /dev/null
+++ b/paging-state/src/commonMain/kotlin/dev/icerock/moko/paging/state/ResourceState.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package dev.icerock.moko.paging.state
+
+typealias ResourceStateThrow = ResourceState
+
+sealed interface ResourceState {
+ object Empty : ResourceState
+ object Loading : ResourceState
+ data class Data(val data: T) : ResourceState
+ data class Error(val error: E) : ResourceState
+
+ fun dataValue(): T? = (this as? Data)?.data
+
+ val isEmpty: Boolean get() = this is Empty
+ val isLoading: Boolean get() = this is Loading
+ val isError: Boolean get() = this is Error<*>
+ val isData: Boolean get() = this is Data<*>
+}
diff --git a/paging-state/src/commonMain/kotlin/dev/icerock/moko/paging/state/ResourceStateExt.kt b/paging-state/src/commonMain/kotlin/dev/icerock/moko/paging/state/ResourceStateExt.kt
new file mode 100644
index 0000000..e8741aa
--- /dev/null
+++ b/paging-state/src/commonMain/kotlin/dev/icerock/moko/paging/state/ResourceStateExt.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package dev.icerock.moko.paging.state
+
+fun T.asState(): ResourceState =
+ ResourceState.Data(this)
+
+fun T?.asState(whenNull: () -> ResourceState): ResourceState =
+ this?.asState() ?: whenNull()
+
+fun List.asState(): ResourceState, E> = if (this.isEmpty()) {
+ ResourceState.Empty
+} else {
+ ResourceState.Data(this)
+}
+
+fun E.asState(): ResourceState, E> =
+ ResourceState.Error(this)
+
+fun List?.asState(whenNull: () -> ResourceState, E>): ResourceState, E> =
+ this?.asState() ?: whenNull()
+
+inline fun ResourceState?.nullAsEmpty(): ResourceState =
+ this ?: ResourceState.Empty
+
+inline fun ResourceState?.nullAsLoading(): ResourceState =
+ this ?: ResourceState.Loading
diff --git a/paging-test/build.gradle.kts b/paging-test/build.gradle.kts
new file mode 100644
index 0000000..0c416f5
--- /dev/null
+++ b/paging-test/build.gradle.kts
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+plugins {
+ id("dev.icerock.moko.gradle.multiplatform.mobile")
+ id("dev.icerock.moko.gradle.publication")
+ id("dev.icerock.moko.gradle.stub.javadoc")
+ id("dev.icerock.moko.gradle.detekt")
+}
+
+dependencies {
+ commonMainApi(projects.pagingCore)
+ commonMainApi(libs.kotlinTestJUnit)
+ commonMainApi(libs.ktorClient)
+ commonMainApi(libs.ktorClientMock)
+
+ androidMainApi(libs.androidCoreTesting)
+ iosMainApi(libs.coroutines)
+}
diff --git a/paging-test/src/androidMain/AndroidManifest.xml b/paging-test/src/androidMain/AndroidManifest.xml
new file mode 100755
index 0000000..25f57e7
--- /dev/null
+++ b/paging-test/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/paging/src/androidTest/kotlin/dev/icerock/moko/paging/BaseTestsClass.kt b/paging-test/src/androidMain/kotlin/dev/icerock/moko/paging/test/BaseTestsClass.kt
similarity index 88%
rename from paging/src/androidTest/kotlin/dev/icerock/moko/paging/BaseTestsClass.kt
rename to paging-test/src/androidMain/kotlin/dev/icerock/moko/paging/test/BaseTestsClass.kt
index 92c92a1..f9da17a 100644
--- a/paging/src/androidTest/kotlin/dev/icerock/moko/paging/BaseTestsClass.kt
+++ b/paging-test/src/androidMain/kotlin/dev/icerock/moko/paging/test/BaseTestsClass.kt
@@ -2,7 +2,7 @@
* Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/
-package dev.icerock.moko.paging
+package dev.icerock.moko.paging.test
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import org.junit.Rule
diff --git a/paging/src/androidTest/kotlin/dev/icerock/moko/paging/UtilsJvm.kt b/paging-test/src/androidMain/kotlin/dev/icerock/moko/paging/test/UtilsJvm.kt
similarity index 87%
rename from paging/src/androidTest/kotlin/dev/icerock/moko/paging/UtilsJvm.kt
rename to paging-test/src/androidMain/kotlin/dev/icerock/moko/paging/test/UtilsJvm.kt
index 402ebf0..bd10e3c 100644
--- a/paging/src/androidTest/kotlin/dev/icerock/moko/paging/UtilsJvm.kt
+++ b/paging-test/src/androidMain/kotlin/dev/icerock/moko/paging/test/UtilsJvm.kt
@@ -2,7 +2,7 @@
* Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/
-package dev.icerock.moko.paging
+package dev.icerock.moko.paging.test
import kotlinx.coroutines.CoroutineScope
diff --git a/paging/src/commonTest/kotlin/dev/icerock/moko/paging/BaseTestsClass.kt b/paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test/BaseTestsClass.kt
similarity index 79%
rename from paging/src/commonTest/kotlin/dev/icerock/moko/paging/BaseTestsClass.kt
rename to paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test/BaseTestsClass.kt
index 9237c01..622385f 100644
--- a/paging/src/commonTest/kotlin/dev/icerock/moko/paging/BaseTestsClass.kt
+++ b/paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test/BaseTestsClass.kt
@@ -2,6 +2,6 @@
* Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/
-package dev.icerock.moko.paging
+package dev.icerock.moko.paging.test
expect open class BaseTestsClass()
diff --git a/paging/src/commonTest/kotlin/dev/icerock/moko/paging/IntegrationTests.kt b/paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test/IntegrationTests.kt
similarity index 58%
rename from paging/src/commonTest/kotlin/dev/icerock/moko/paging/IntegrationTests.kt
rename to paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test/IntegrationTests.kt
index 13e40f8..9716365 100644
--- a/paging/src/commonTest/kotlin/dev/icerock/moko/paging/IntegrationTests.kt
+++ b/paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test/IntegrationTests.kt
@@ -1,15 +1,20 @@
/*
- * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/
+@file:Suppress("Indentation")
-package dev.icerock.moko.paging
+package dev.icerock.moko.paging.test
+import dev.icerock.moko.paging.core.LambdaPagedListDataSource
+import dev.icerock.moko.paging.core.PagedListDataSource
+import dev.icerock.moko.paging.core.Pagination
import io.ktor.client.HttpClient
import io.ktor.client.engine.mock.MockEngine
import io.ktor.client.engine.mock.respondOk
import io.ktor.client.request.get
-import io.ktor.client.statement.*
+import io.ktor.client.statement.bodyAsText
import io.ktor.http.fullPath
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
@@ -17,14 +22,39 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.test.Test
-class IntegrationTests : BaseTestsClass() {
+abstract class IntegrationTests : BaseTestsClass() {
+
+ abstract fun createPagination(
+ parentScope: CoroutineScope,
+ dataSource: PagedListDataSource = paginationDataSource,
+ comparator: Comparator = itemsComparator,
+ nextPageListener: (Result>) -> Unit = {},
+ refreshListener: (Result>) -> Unit = {},
+ ): Pagination
+
+ private val paginationDataSource: LambdaPagedListDataSource =
+ LambdaPagedListDataSource {
+ println("start load new page with $it")
+ val randomJoke: String = httpClient
+ .get(API_URL)
+ .bodyAsText()
+ println("respond new item $randomJoke")
+ listOf(randomJoke)
+ }
+
+ private val itemsComparator = Comparator { a: String, b: String ->
+ a.compareTo(b)
+ }
+
+ @Suppress("MaxLineLength")
private val httpClient = HttpClient(MockEngine) {
engine {
addHandler { request ->
- when (request.url.fullPath) {
- "http://api.icndb.com/jokes/random" -> {
+ when (request.url.toString()) {
+ API_URL -> {
delay(200)
- respondOk("""
+ respondOk(
+ """
{
"type": "success",
"value": {
@@ -33,7 +63,8 @@ class IntegrationTests : BaseTestsClass() {
"categories": []
}
}
- """.trimIndent())
+ """.trimIndent()
+ )
}
else -> error("Unhandled ${request.url.fullPath}")
}
@@ -42,22 +73,8 @@ class IntegrationTests : BaseTestsClass() {
}
@Test
- fun parallelRequests() = runTest {
- val pagination = Pagination(
- parentScope = this,
- dataSource = LambdaPagedListDataSource {
- println("start load new page with $it")
- val randomJoke: String = httpClient
- .get("http://api.icndb.com/jokes/random")
- .bodyAsText()
-
- println("respond new item $randomJoke")
- listOf(randomJoke)
- },
- comparator = Comparator { a, b -> a.compareTo(b) },
- nextPageListener = { },
- refreshListener = { }
- )
+ open fun parallelRequests() = runTest {
+ val pagination: Pagination = createPagination(this)
for (i in 0..10) {
println("--- ITERATION $i START ---")
@@ -83,22 +100,8 @@ class IntegrationTests : BaseTestsClass() {
}
@Test
- fun parallelRequestsAndSetData() = runTest {
- val pagination = Pagination(
- parentScope = this,
- dataSource = LambdaPagedListDataSource {
- println("start load new page with $it")
- val randomJoke: String = httpClient
- .get("http://api.icndb.com/jokes/random")
- .bodyAsText()
-
- println("respond new item $randomJoke")
- listOf(randomJoke)
- },
- comparator = Comparator { a, b -> a.compareTo(b) },
- nextPageListener = { },
- refreshListener = { }
- )
+ open fun parallelRequestsAndSetData() = runTest {
+ val pagination = createPagination(this)
for (i in 0..10) {
println("--- ITERATION $i START ---")
@@ -120,7 +123,7 @@ class IntegrationTests : BaseTestsClass() {
},
async {
println("--> $it set data start")
- val data = pagination.state.value.dataValue().orEmpty()
+ val data = pagination.dataValue().orEmpty()
val newData = data.plus("new item")
pagination.setDataSuspend(newData)
println("--> $it set data end")
@@ -131,24 +134,10 @@ class IntegrationTests : BaseTestsClass() {
}
@Test
- fun closingScope() = runTest {
+ open fun closingScope() = runTest {
val exc = runCatching {
coroutineScope {
- val pagination = Pagination(
- parentScope = this,
- dataSource = LambdaPagedListDataSource {
- println("start load new page with $it")
- val randomJoke: String = httpClient
- .get("http://api.icndb.com/jokes/random")
- .bodyAsText()
-
- println("respond new item $randomJoke")
- listOf(randomJoke)
- },
- comparator = Comparator { a, b -> a.compareTo(b) },
- nextPageListener = { },
- refreshListener = { }
- )
+ val pagination = createPagination(this)
launch {
println("start load")
@@ -164,4 +153,8 @@ class IntegrationTests : BaseTestsClass() {
println(exc)
}
-}
\ No newline at end of file
+
+ companion object {
+ const val API_URL = "http://api.icndb.com/jokes/random"
+ }
+}
diff --git a/paging/src/commonTest/kotlin/dev/icerock/moko/paging/PaginationTest.kt b/paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test/PaginationTest.kt
similarity index 52%
rename from paging/src/commonTest/kotlin/dev/icerock/moko/paging/PaginationTest.kt
rename to paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test/PaginationTest.kt
index bedb7cb..e061536 100644
--- a/paging/src/commonTest/kotlin/dev/icerock/moko/paging/PaginationTest.kt
+++ b/paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test/PaginationTest.kt
@@ -1,9 +1,12 @@
/*
- * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/
-package dev.icerock.moko.paging
+package dev.icerock.moko.paging.test
+import dev.icerock.moko.paging.core.LambdaPagedListDataSource
+import dev.icerock.moko.paging.core.PagedListDataSource
+import dev.icerock.moko.paging.core.Pagination
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
@@ -11,67 +14,77 @@ import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertTrue
-class PaginationTest : BaseTestsClass() {
+abstract class PaginationTest : BaseTestsClass() {
- var paginationDataSource = TestListDataSource(3, 5)
+ abstract fun createPagination(
+ parentScope: CoroutineScope,
+ dataSource: PagedListDataSource = paginationDataSource,
+ comparator: Comparator = itemsComparator,
+ nextPageListener: (Result>) -> Unit = {},
+ refreshListener: (Result>) -> Unit = {},
+ ): Pagination
+
+ abstract fun isSuccessState(pagination: Pagination): Boolean
+
+ private var paginationDataSource = TestListDataSource(3, 5)
- val itemsComparator = Comparator { a: Int, b: Int ->
+ private val itemsComparator = Comparator { a: Int, b: Int ->
a - b
}
@BeforeTest
- fun setup() {
+ open fun setup() {
paginationDataSource = TestListDataSource(3, 5)
}
@Test
- fun `load first page`() = runTest {
- val pagination = createPagination()
+ open fun `load first page`() = runTest {
+ val pagination = createPagination(this)
pagination.loadFirstPageSuspend()
assertTrue {
- pagination.state.value.isSuccess()
+ isSuccessState(pagination)
}
assertTrue {
- pagination.state.value.dataValue()!!.compareWith(listOf(0, 1, 2))
+ pagination.dataValue()!!.compareWith(listOf(0, 1, 2))
}
}
@Test
- fun `load next page`() = runTest {
- val pagination = createPagination()
+ open fun `load next page`() = runTest {
+ val pagination = createPagination(this)
pagination.loadFirstPageSuspend()
pagination.loadNextPageSuspend()
assertTrue {
- pagination.state.value.dataValue()!!.compareWith(listOf(0, 1, 2, 3, 4, 5))
+ pagination.dataValue()!!.compareWith(listOf(0, 1, 2, 3, 4, 5))
}
pagination.loadNextPageSuspend()
assertTrue {
- pagination.state.value.dataValue()!!.compareWith(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8))
+ pagination.dataValue()!!.compareWith(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8))
}
}
@Test
- fun `refresh pagination`() = runTest {
- val pagination = createPagination()
+ open fun `refresh pagination`() = runTest {
+ val pagination = createPagination(this)
pagination.loadFirstPageSuspend()
pagination.loadNextPageSuspend()
pagination.refreshSuspend()
assertTrue {
- pagination.state.value.dataValue()!!.compareWith(listOf(0, 1, 2))
+ pagination.dataValue()!!.compareWith(listOf(0, 1, 2))
}
}
@Test
- fun `set data`() = runTest {
- val pagination = createPagination()
+ open fun `set data`() = runTest {
+ val pagination = createPagination(this)
pagination.loadFirstPageSuspend()
pagination.loadNextPageSuspend()
@@ -80,25 +93,22 @@ class PaginationTest : BaseTestsClass() {
pagination.setDataSuspend(setList)
assertTrue {
- pagination.state.value.dataValue()!!.compareWith(setList)
+ pagination.dataValue()!!.compareWith(setList)
}
}
@Test
- fun `double refresh`() = runTest {
+ open fun `double refresh`() = runTest {
var counter = 0
- val pagination = Pagination(
- parentScope = this,
+ val pagination = createPagination(
+ this,
dataSource = LambdaPagedListDataSource {
val load = counter++
println("start load new page with $it")
delay(100)
println("respond new list $load")
listOf(1, 2, 3, 4)
- },
- comparator = itemsComparator,
- nextPageListener = { },
- refreshListener = { }
+ }
)
println("start load first page")
@@ -118,15 +128,4 @@ class PaginationTest : BaseTestsClass() {
r1.await()
r2.await()
}
-
- private fun CoroutineScope.createPagination(
- nextPageListener: (Result>) -> Unit = {},
- refreshListener: (Result>) -> Unit = {}
- ) = Pagination(
- parentScope = this,
- dataSource = paginationDataSource,
- comparator = itemsComparator,
- nextPageListener = nextPageListener,
- refreshListener = refreshListener
- )
}
diff --git a/paging/src/commonTest/kotlin/dev/icerock/moko/paging/TestListDataSource.kt b/paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test/TestListDataSource.kt
similarity index 71%
rename from paging/src/commonTest/kotlin/dev/icerock/moko/paging/TestListDataSource.kt
rename to paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test/TestListDataSource.kt
index f672a23..e655912 100644
--- a/paging/src/commonTest/kotlin/dev/icerock/moko/paging/TestListDataSource.kt
+++ b/paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test/TestListDataSource.kt
@@ -2,10 +2,12 @@
* Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/
-package dev.icerock.moko.paging
+package dev.icerock.moko.paging.test
+
+import dev.icerock.moko.paging.core.PagedListDataSource
class TestListDataSource(val pageSize: Int, val totalPagesCount: Int) : PagedListDataSource {
- val dataList = (0 .. pageSize * totalPagesCount).map { it }
+ val dataList = (0..pageSize * totalPagesCount).map { it }
override suspend fun loadPage(currentList: List?): List {
val offset = currentList?.size ?: 0
diff --git a/paging/src/commonTest/kotlin/dev/icerock/moko/paging/Utils.kt b/paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test/Utils.kt
similarity index 81%
rename from paging/src/commonTest/kotlin/dev/icerock/moko/paging/Utils.kt
rename to paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test/Utils.kt
index c958dc8..53f9db3 100644
--- a/paging/src/commonTest/kotlin/dev/icerock/moko/paging/Utils.kt
+++ b/paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test/Utils.kt
@@ -2,14 +2,14 @@
* Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/
-package dev.icerock.moko.paging
+package dev.icerock.moko.paging.test
import kotlinx.coroutines.CoroutineScope
expect fun runTest(block: suspend CoroutineScope.() -> T): T
fun List.compareWith(list: List): Boolean {
- if(size != list.size) return false
+ if (size != list.size) return false
return zip(list).all { (item1, item2) ->
item1 == item2
diff --git a/paging/src/iosTest/kotlin/dev/icerock/moko/paging/BaseTestsClass.kt b/paging-test/src/iosMain/kotlin/dev/icerock/moko/paging/test/BaseTestsClass.kt
similarity index 79%
rename from paging/src/iosTest/kotlin/dev/icerock/moko/paging/BaseTestsClass.kt
rename to paging-test/src/iosMain/kotlin/dev/icerock/moko/paging/test/BaseTestsClass.kt
index 16393af..49d5a5e 100644
--- a/paging/src/iosTest/kotlin/dev/icerock/moko/paging/BaseTestsClass.kt
+++ b/paging-test/src/iosMain/kotlin/dev/icerock/moko/paging/test/BaseTestsClass.kt
@@ -2,6 +2,6 @@
* Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/
-package dev.icerock.moko.paging
+package dev.icerock.moko.paging.test
actual open class BaseTestsClass
diff --git a/paging/src/iosTest/kotlin/dev/icerock/moko/paging/Utils.kt b/paging-test/src/iosMain/kotlin/dev/icerock/moko/paging/test/Utils.kt
similarity index 92%
rename from paging/src/iosTest/kotlin/dev/icerock/moko/paging/Utils.kt
rename to paging-test/src/iosMain/kotlin/dev/icerock/moko/paging/test/Utils.kt
index ddc8236..815a842 100644
--- a/paging/src/iosTest/kotlin/dev/icerock/moko/paging/Utils.kt
+++ b/paging-test/src/iosMain/kotlin/dev/icerock/moko/paging/test/Utils.kt
@@ -2,13 +2,12 @@
* Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/
-package dev.icerock.moko.paging
+package dev.icerock.moko.paging.test
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Delay
-import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.Runnable
@@ -29,8 +28,8 @@ actual fun runTest(block: suspend CoroutineScope.() -> T): T {
expectation.fulfill(kotlin.runCatching { block.invoke(this) })
}
- val result: Result? = expectation.wait()
- if (result == null) throw RuntimeException("runBlocking failed")
+ val result: Result = expectation.wait()
+ ?: throw RuntimeException("runBlocking failed")
return result.getOrThrow()
}
diff --git a/paging/src/androidMain/AndroidManifest.xml b/paging/src/androidMain/AndroidManifest.xml
deleted file mode 100755
index c441c39..0000000
--- a/paging/src/androidMain/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/paging/src/iosMain/kotlin/Dummy.kt b/paging/src/iosMain/kotlin/Dummy.kt
deleted file mode 100644
index 97f62dc..0000000
--- a/paging/src/iosMain/kotlin/Dummy.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-/*
- * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package dev.icerock.moko.paging
-
-// required for produce `metadata/iosMain`
-internal val sDummyVar: Int? = null
diff --git a/paging/src/jvmTest/kotlin/dev/icerock/moko/paging/BaseTestsClass.kt b/paging/src/jvmTest/kotlin/dev/icerock/moko/paging/BaseTestsClass.kt
deleted file mode 100644
index 1871835..0000000
--- a/paging/src/jvmTest/kotlin/dev/icerock/moko/paging/BaseTestsClass.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-/*
- * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package dev.icerock.moko.paging
-
-actual open class BaseTestsClass
diff --git a/paging/src/jvmTest/kotlin/dev/icerock/moko/paging/runTest.kt b/paging/src/jvmTest/kotlin/dev/icerock/moko/paging/runTest.kt
deleted file mode 100644
index c6437bb..0000000
--- a/paging/src/jvmTest/kotlin/dev/icerock/moko/paging/runTest.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package dev.icerock.moko.paging
-
-import kotlinx.coroutines.CoroutineScope
-
-actual fun runTest(block: suspend CoroutineScope.() -> T): T =
- kotlinx.coroutines.runBlocking(block = block)
diff --git a/sample-declarative-ui/androidApp/build.gradle.kts b/sample-declarative-ui/androidApp/build.gradle.kts
new file mode 100644
index 0000000..04352a7
--- /dev/null
+++ b/sample-declarative-ui/androidApp/build.gradle.kts
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+plugins {
+ id("com.android.application")
+ kotlin("android")
+}
+
+val composeVersion = "1.2.0-beta03"
+
+android {
+ compileSdk = 32
+ buildFeatures {
+ compose = true
+ }
+ defaultConfig {
+ applicationId = "dev.icerock.moko.paging.sample.declarativeui.android"
+ minSdk = 21
+ targetSdk = 32
+ versionCode = 1
+ versionName = "1.0"
+ }
+ buildTypes {
+ getByName("release") {
+ isMinifyEnabled = false
+ }
+ }
+
+ kotlinOptions {
+ freeCompilerArgs += listOf(
+ "-P",
+ "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"
+ )
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = composeVersion
+ }
+}
+
+dependencies {
+ implementation(project(":sample-declarative-ui:shared"))
+
+
+ implementation("com.google.accompanist:accompanist-swiperefresh:0.24.7-alpha")
+ implementation("androidx.compose.ui:ui:$composeVersion")
+ // Tooling support (Previews, etc.)
+ implementation("androidx.compose.ui:ui-tooling:$composeVersion")
+ // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.)
+ implementation("androidx.compose.foundation:foundation:$composeVersion")
+ // Material Design
+ implementation("androidx.compose.material:material:$composeVersion")
+ // Material design icons
+ implementation("androidx.compose.material:material-icons-core:$composeVersion")
+ // Integration with observables
+ implementation("androidx.compose.runtime:runtime-livedata:$composeVersion")
+
+ implementation("androidx.activity:activity-compose:1.4.0")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1")
+ implementation("androidx.navigation:navigation-compose:2.4.2")
+}
diff --git a/sample-declarative-ui/androidApp/src/main/AndroidManifest.xml b/sample-declarative-ui/androidApp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..db0d83a
--- /dev/null
+++ b/sample-declarative-ui/androidApp/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/paging/sample/declarativeui/android/BindingExt.kt b/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/paging/sample/declarativeui/android/BindingExt.kt
new file mode 100644
index 0000000..2f6852f
--- /dev/null
+++ b/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/paging/sample/declarativeui/android/BindingExt.kt
@@ -0,0 +1,32 @@
+package dev.icerock.moko.paging.sample.declarativeui.android
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
+import androidx.compose.runtime.collectAsState
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+class MutableStateAdapter(
+ private val state: State,
+ private val mutate: (T) -> Unit
+) : MutableState {
+
+ override var value: T
+ get() = state.value
+ set(value) {
+ mutate(value)
+ }
+
+ override fun component1(): T = value
+ override fun component2(): (T) -> Unit = { value = it }
+}
+
+@Composable
+fun MutableStateFlow.collectAsMutableState(
+ context: CoroutineContext = EmptyCoroutineContext
+): MutableState = MutableStateAdapter(
+ state = collectAsState(context),
+ mutate = { value = it }
+)
\ No newline at end of file
diff --git a/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/paging/sample/declarativeui/android/ComposeApp.kt b/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/paging/sample/declarativeui/android/ComposeApp.kt
new file mode 100644
index 0000000..9b664ed
--- /dev/null
+++ b/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/paging/sample/declarativeui/android/ComposeApp.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package dev.icerock.moko.paging.sample.declarativeui.android
+
+import androidx.compose.runtime.Composable
+import androidx.navigation.NavOptions
+import androidx.navigation.compose.NavHost
+import androidx.compose.material.Text
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+
+@Composable
+fun ComposeApp() {
+ val navController = rememberNavController()
+
+ NavHost(navController = navController, startDestination = "list") {
+ composable("list") {
+ ListScreen()
+ }
+ }
+}
diff --git a/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/paging/sample/declarativeui/android/ListScreen.kt b/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/paging/sample/declarativeui/android/ListScreen.kt
new file mode 100644
index 0000000..cd3e8ca
--- /dev/null
+++ b/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/paging/sample/declarativeui/android/ListScreen.kt
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package dev.icerock.moko.paging.sample.declarativeui.android
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.CircularProgressIndicator
+import androidx.compose.material.OutlinedButton
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.google.accompanist.swiperefresh.SwipeRefresh
+import com.google.accompanist.swiperefresh.SwipeRefreshState
+import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
+import dev.icerock.moko.mvvm.createViewModelFactory
+import dev.icerock.moko.paging.sample.declarativeui.ListUnit
+import dev.icerock.moko.paging.sample.declarativeui.ListViewModel
+import dev.icerock.moko.paging.state.ResourceState
+
+@Composable
+fun ListScreen(
+ viewModel: ListViewModel = viewModel(
+ factory = createViewModelFactory {
+ ListViewModel(
+ withError = false,
+ withInitialValue = false
+ )
+ .apply { start() }
+ }
+ )
+) {
+
+ val state: ResourceState, String> by viewModel.state.collectAsState()
+ val isRefreshing: Boolean by viewModel.isRefreshing.collectAsState()
+
+ ListStateBody(
+ state = state,
+ onRetryPressed = { viewModel.onRetryPressed() },
+ isRefreshing = isRefreshing,
+ onRefresh = { viewModel.onRefresh() },
+ loadNextPage = { viewModel.onLoadNextPage() }
+ )
+}
+
+@Composable
+fun ListStateBody(
+ state: ResourceState, String>,
+ onRefresh: () -> Unit,
+ onRetryPressed: () -> Unit,
+ loadNextPage: () -> Unit,
+ isRefreshing: Boolean
+) {
+ val refreshState: SwipeRefreshState = rememberSwipeRefreshState(isRefreshing = isRefreshing)
+ SwipeRefresh(
+ state = refreshState,
+ onRefresh = onRefresh
+ ) {
+ when (state) {
+ is ResourceState.Data -> ListStateContent(
+ data = state.data,
+ loadNextPage = loadNextPage
+ )
+ ResourceState.Empty -> EmptyStateView()
+ is ResourceState.Error -> ErrorStateView(
+ message = state.error,
+ onRetryPressed = onRetryPressed
+ )
+ ResourceState.Loading -> LoadingStateView()
+ }
+ }
+
+}
+
+@Composable fun ListStateContent(
+ data: List,
+ loadNextPage: () -> Unit
+) {
+ val state: LazyListState = rememberLazyListState()
+ println(remember { derivedStateOf { state.layoutInfo } })
+
+ LazyColumn(
+ state = state,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ itemsIndexed(
+ items = data
+ ) { index, unit ->
+ if (index > 0.7 * data.size) {
+ loadNextPage()
+ }
+ ListItem(unit)
+ }
+ }
+}
+
+@Composable
+fun ListItem(unit: ListUnit) {
+ when (unit) {
+ ListUnit.Loading -> LoadingListItem()
+ is ListUnit.ProductUnit -> ProductListItem(unit)
+ }
+}
+
+@Composable
+fun LoadingListItem() {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .width(15.dp)
+ ) {
+ CircularProgressIndicator(
+ modifier = Modifier
+ .align(Alignment.Center)
+ .padding()
+ )
+ }
+}
+
+@Composable
+fun ProductListItem(unit: ListUnit.ProductUnit) {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.Start
+ ) {
+ Text(modifier = Modifier.padding(16.dp), text = unit.title)
+ }
+}
+
+@Composable
+fun LoadingStateView() {
+ Box(modifier = Modifier.fillMaxSize()) {
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+ }
+}
+
+@Composable
+private fun EmptyStateView() {
+ Box(Modifier.fillMaxSize()) {
+ Text(modifier = Modifier.align(Alignment.Center), text = "Empty")
+ }
+}
+
+@Composable
+private fun ErrorStateView(
+ message: String,
+ onRetryPressed: (() -> Unit)?
+) {
+ Box(Modifier.fillMaxSize()) {
+ Column(
+ modifier = Modifier.align(Alignment.Center),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = message,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(horizontal = 16.dp)
+ )
+ if (onRetryPressed != null) {
+ Spacer(modifier = Modifier.height(16.dp))
+ OutlinedButton(onClick = onRetryPressed) {
+ Text(
+ text = "Retry",
+ )
+ }
+ }
+ }
+ }
+}
+
+@Preview(showSystemUi = true, group = "load")
+@Composable
+fun ListStatePreview() {
+ ListStateBody(
+ state = ResourceState.Data(
+ data = getTestData()
+ ),
+ onRefresh = {},
+ onRetryPressed = {},
+ isRefreshing = true,
+ loadNextPage = {}
+ )
+}
+
+@Preview(showSystemUi = true, group = "load")
+@Composable
+fun ListStateLoadingPreview() {
+ ListStateBody(
+ state = ResourceState.Loading,
+ onRefresh = {},
+ onRetryPressed = {},
+ isRefreshing = false,
+ loadNextPage = {}
+ )
+}
+
+@Preview(showSystemUi = true, group = "load")
+@Composable
+fun ListStateEmptyPreview() {
+ ListStateBody(
+ state = ResourceState.Empty,
+ onRefresh = {},
+ onRetryPressed = {},
+ isRefreshing = false,
+ loadNextPage = {}
+ )
+}
+
+@Preview(showSystemUi = true, group = "load")
+@Composable
+fun ListStateErrorPreview() {
+ ListStateBody(
+ state = ResourceState.Error(error = "Some error"),
+ onRefresh = {},
+ onRetryPressed = {},
+ isRefreshing = false,
+ loadNextPage = {}
+ )
+}
+
+fun getTestData(withLoading: Boolean = true): List =
+ (1..5).map {
+ ListUnit.ProductUnit(
+ title = "Product #$it",
+ id = it.toLong()
+ )
+ } + if (withLoading) listOf(ListUnit.Loading) else emptyList()
+
diff --git a/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/paging/sample/declarativeui/android/MainActivity.kt b/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/paging/sample/declarativeui/android/MainActivity.kt
new file mode 100644
index 0000000..b2fc0d8
--- /dev/null
+++ b/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/paging/sample/declarativeui/android/MainActivity.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package dev.icerock.moko.paging.sample.declarativeui.android
+
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.material.MaterialTheme
+import dev.icerock.moko.paging.sample.declarativeui.android.ComposeApp
+
+class MainActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ MaterialTheme {
+ ComposeApp()
+ }
+ }
+ }
+}
diff --git a/sample-declarative-ui/androidApp/src/main/res/values/colors.xml b/sample-declarative-ui/androidApp/src/main/res/values/colors.xml
new file mode 100644
index 0000000..4faecfa
--- /dev/null
+++ b/sample-declarative-ui/androidApp/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #6200EE
+ #3700B3
+ #03DAC5
+
\ No newline at end of file
diff --git a/sample-declarative-ui/androidApp/src/main/res/values/styles.xml b/sample-declarative-ui/androidApp/src/main/res/values/styles.xml
new file mode 100644
index 0000000..1971a0a
--- /dev/null
+++ b/sample-declarative-ui/androidApp/src/main/res/values/styles.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/sample-declarative-ui/iosApp/Podfile b/sample-declarative-ui/iosApp/Podfile
new file mode 100644
index 0000000..4c27b0a
--- /dev/null
+++ b/sample-declarative-ui/iosApp/Podfile
@@ -0,0 +1,9 @@
+platform :ios, '13.0'
+
+target 'iosApp' do
+ # Comment the next line if you don't want to use dynamic frameworks
+ use_frameworks!
+
+ # Pods for iosApp
+ pod 'mokoMvvmFlowSwiftUI', :podspec => 'https://raw.githubusercontent.com/icerockdev/moko-mvvm/release/0.13.0/mokoMvvmFlowSwiftUI.podspec'
+end
diff --git a/sample-declarative-ui/iosApp/Podfile.lock b/sample-declarative-ui/iosApp/Podfile.lock
new file mode 100644
index 0000000..40819c9
--- /dev/null
+++ b/sample-declarative-ui/iosApp/Podfile.lock
@@ -0,0 +1,16 @@
+PODS:
+ - mokoMvvmFlowSwiftUI (0.13.0)
+
+DEPENDENCIES:
+ - mokoMvvmFlowSwiftUI (from `https://raw.githubusercontent.com/icerockdev/moko-mvvm/release/0.13.0/mokoMvvmFlowSwiftUI.podspec`)
+
+EXTERNAL SOURCES:
+ mokoMvvmFlowSwiftUI:
+ :podspec: https://raw.githubusercontent.com/icerockdev/moko-mvvm/release/0.13.0/mokoMvvmFlowSwiftUI.podspec
+
+SPEC CHECKSUMS:
+ mokoMvvmFlowSwiftUI: 64572433b11ad2512ddc16141fc64cf8b958c675
+
+PODFILE CHECKSUM: c2128edf4dd169ca3a3bce4ebdb28c636800d06f
+
+COCOAPODS: 1.11.3
diff --git a/sample-declarative-ui/iosApp/iosApp.xcodeproj/project.pbxproj b/sample-declarative-ui/iosApp/iosApp.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..d3c11c2
--- /dev/null
+++ b/sample-declarative-ui/iosApp/iosApp.xcodeproj/project.pbxproj
@@ -0,0 +1,476 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 54;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; };
+ 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; };
+ 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };
+ 22BDE752281BEE8000259368 /* MultiPlatformLibrary.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22BDE746281BEDBA00259368 /* MultiPlatformLibrary.xcframework */; };
+ 22BDE753281BEE8000259368 /* MultiPlatformLibrary.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 22BDE746281BEDBA00259368 /* MultiPlatformLibrary.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 45061679284861C9005F99DF /* dev_icerock_moko_shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45061676284861C9005F99DF /* dev_icerock_moko_shared.swift */; };
+ 4506167A284861C9005F99DF /* dev_icerock_moko_mvvm-flow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45061677284861C9005F99DF /* dev_icerock_moko_mvvm-flow.swift */; };
+ 4506167D284862DE005F99DF /* ListBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4506167C284862DE005F99DF /* ListBinding.swift */; };
+ 4506167F28486E3D005F99DF /* ListScreenBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4506167E28486E3D005F99DF /* ListScreenBody.swift */; };
+ 4506168128486EB5005F99DF /* mokoStringDescExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4506168028486EB5005F99DF /* mokoStringDescExt.swift */; };
+ 454B4D4C284A91DF0086F4AC /* dev_icerock_moko_resources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 454B4D4B284A91DF0086F4AC /* dev_icerock_moko_resources.swift */; };
+ 4579DEF5284A8EC9004961C5 /* dev_icerock_moko_paging-state.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4579DEF4284A8EC9004961C5 /* dev_icerock_moko_paging-state.swift */; };
+ 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; };
+ FCF4E6E3EA1204EC05EEF872 /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 44E36C38F63DF79F7CEEAE3E /* Pods_iosApp.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 7555FFB4242A642300829871 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ 22BDE753281BEE8000259368 /* MultiPlatformLibrary.xcframework in Embed Frameworks */,
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
+ 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; };
+ 22BDE746281BEDBA00259368 /* MultiPlatformLibrary.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MultiPlatformLibrary.xcframework; path = ../shared/build/xcode/MultiPlatformLibrary.xcframework; sourceTree = ""; };
+ 44E36C38F63DF79F7CEEAE3E /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 45061676284861C9005F99DF /* dev_icerock_moko_shared.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = dev_icerock_moko_shared.swift; sourceTree = ""; };
+ 45061677284861C9005F99DF /* dev_icerock_moko_mvvm-flow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "dev_icerock_moko_mvvm-flow.swift"; sourceTree = ""; };
+ 4506167C284862DE005F99DF /* ListBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListBinding.swift; sourceTree = ""; };
+ 4506167E28486E3D005F99DF /* ListScreenBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListScreenBody.swift; sourceTree = ""; };
+ 4506168028486EB5005F99DF /* mokoStringDescExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mokoStringDescExt.swift; sourceTree = ""; };
+ 454B4D4B284A91DF0086F4AC /* dev_icerock_moko_resources.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = dev_icerock_moko_resources.swift; sourceTree = ""; };
+ 4579DEF4284A8EC9004961C5 /* dev_icerock_moko_paging-state.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "dev_icerock_moko_paging-state.swift"; sourceTree = ""; };
+ 4745F88F44B963D7A59D2180 /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.release.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig"; sourceTree = ""; };
+ 7555FF7B242A565900829871 /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
+ 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ C67D2807155711FDB9BBDD09 /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 7555FF78242A565900829871 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 22BDE752281BEE8000259368 /* MultiPlatformLibrary.xcframework in Frameworks */,
+ FCF4E6E3EA1204EC05EEF872 /* Pods_iosApp.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 058557D7273AAEEB004C7B11 /* Preview Content */ = {
+ isa = PBXGroup;
+ children = (
+ 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */,
+ );
+ path = "Preview Content";
+ sourceTree = "";
+ };
+ 140DDE5BD0BCDADF6482A380 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ C67D2807155711FDB9BBDD09 /* Pods-iosApp.debug.xcconfig */,
+ 4745F88F44B963D7A59D2180 /* Pods-iosApp.release.xcconfig */,
+ );
+ path = Pods;
+ sourceTree = "";
+ };
+ 45061675284861C9005F99DF /* fromMpp */ = {
+ isa = PBXGroup;
+ children = (
+ 454B4D4B284A91DF0086F4AC /* dev_icerock_moko_resources.swift */,
+ 4579DEF4284A8EC9004961C5 /* dev_icerock_moko_paging-state.swift */,
+ 45061676284861C9005F99DF /* dev_icerock_moko_shared.swift */,
+ 45061677284861C9005F99DF /* dev_icerock_moko_mvvm-flow.swift */,
+ );
+ path = fromMpp;
+ sourceTree = "";
+ };
+ 7555FF72242A565900829871 = {
+ isa = PBXGroup;
+ children = (
+ 22BDE746281BEDBA00259368 /* MultiPlatformLibrary.xcframework */,
+ 7555FF7D242A565900829871 /* iosApp */,
+ 7555FF7C242A565900829871 /* Products */,
+ 7555FFB0242A642200829871 /* Frameworks */,
+ 140DDE5BD0BCDADF6482A380 /* Pods */,
+ );
+ sourceTree = "";
+ };
+ 7555FF7C242A565900829871 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 7555FF7B242A565900829871 /* iosApp.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 7555FF7D242A565900829871 /* iosApp */ = {
+ isa = PBXGroup;
+ children = (
+ 4506168028486EB5005F99DF /* mokoStringDescExt.swift */,
+ 45061675284861C9005F99DF /* fromMpp */,
+ 058557BA273AAA24004C7B11 /* Assets.xcassets */,
+ 7555FF82242A565900829871 /* ContentView.swift */,
+ 2152FB032600AC8F00CF470E /* iOSApp.swift */,
+ 4506167C284862DE005F99DF /* ListBinding.swift */,
+ 4506167E28486E3D005F99DF /* ListScreenBody.swift */,
+ 7555FF8C242A565B00829871 /* Info.plist */,
+ 058557D7273AAEEB004C7B11 /* Preview Content */,
+ );
+ path = iosApp;
+ sourceTree = "";
+ };
+ 7555FFB0242A642200829871 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 44E36C38F63DF79F7CEEAE3E /* Pods_iosApp.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 7555FF7A242A565900829871 /* iosApp */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */;
+ buildPhases = (
+ D1546C3CE06A9BC9D549831A /* [CP] Check Pods Manifest.lock */,
+ 7555FFB5242A651A00829871 /* Build Kotlin */,
+ 7555FF77242A565900829871 /* Sources */,
+ 7555FF78242A565900829871 /* Frameworks */,
+ 7555FF79242A565900829871 /* Resources */,
+ 7555FFB4242A642300829871 /* Embed Frameworks */,
+ 6B7FD836AED6380D5CE953CD /* [CP] Embed Pods Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = iosApp;
+ productName = iosApp;
+ productReference = 7555FF7B242A565900829871 /* iosApp.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 7555FF73242A565900829871 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastSwiftUpdateCheck = 1130;
+ LastUpgradeCheck = 1130;
+ ORGANIZATIONNAME = orgName;
+ TargetAttributes = {
+ 7555FF7A242A565900829871 = {
+ CreatedOnToolsVersion = 11.3.1;
+ };
+ };
+ };
+ buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 7555FF72242A565900829871;
+ productRefGroup = 7555FF7C242A565900829871 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 7555FF7A242A565900829871 /* iosApp */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 7555FF79242A565900829871 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */,
+ 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 6B7FD836AED6380D5CE953CD /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 7555FFB5242A651A00829871 /* Build Kotlin */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ name = "Build Kotlin";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "echo $SRCROOT\ncd \"$SRCROOT/../..\"\n./gradlew \"sample-declarative-ui:shared:$BUILD_KOTLIN_GRADLE_TASK\"\n";
+ };
+ D1546C3CE06A9BC9D549831A /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-iosApp-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ 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";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 7555FF77242A565900829871 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 4506167A284861C9005F99DF /* dev_icerock_moko_mvvm-flow.swift in Sources */,
+ 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */,
+ 4506167F28486E3D005F99DF /* ListScreenBody.swift in Sources */,
+ 7555FF83242A565900829871 /* ContentView.swift in Sources */,
+ 4579DEF5284A8EC9004961C5 /* dev_icerock_moko_paging-state.swift in Sources */,
+ 4506167D284862DE005F99DF /* ListBinding.swift in Sources */,
+ 454B4D4C284A91DF0086F4AC /* dev_icerock_moko_resources.swift in Sources */,
+ 45061679284861C9005F99DF /* dev_icerock_moko_shared.swift in Sources */,
+ 4506168128486EB5005F99DF /* mokoStringDescExt.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ 7555FFA3242A565B00829871 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 7555FFA4242A565B00829871 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 7555FFA6242A565B00829871 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = C67D2807155711FDB9BBDD09 /* Pods-iosApp.debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ BUILD_KOTLIN_GRADLE_TASK = syncMultiPlatformLibraryDebugXCFramework;
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
+ ENABLE_PREVIEWS = YES;
+ INFOPLIST_FILE = iosApp/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = orgIdentifier.iosApp;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 7555FFA7242A565B00829871 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 4745F88F44B963D7A59D2180 /* Pods-iosApp.release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ BUILD_KOTLIN_GRADLE_TASK = syncMultiPlatformLibraryReleaseXCFramework;
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
+ ENABLE_PREVIEWS = YES;
+ INFOPLIST_FILE = iosApp/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = orgIdentifier.iosApp;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 7555FFA3242A565B00829871 /* Debug */,
+ 7555FFA4242A565B00829871 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 7555FFA6242A565B00829871 /* Debug */,
+ 7555FFA7242A565B00829871 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 7555FF73242A565900829871 /* Project object */;
+}
diff --git a/sample-declarative-ui/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/sample-declarative-ui/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/sample-declarative-ui/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/sample-declarative-ui/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/sample-declarative-ui/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/sample-declarative-ui/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/sample-declarative-ui/iosApp/iosApp.xcworkspace/contents.xcworkspacedata b/sample-declarative-ui/iosApp/iosApp.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..c009e7d
--- /dev/null
+++ b/sample-declarative-ui/iosApp/iosApp.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/sample-declarative-ui/iosApp/iosApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/sample-declarative-ui/iosApp/iosApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/sample-declarative-ui/iosApp/iosApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/sample-declarative-ui/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/sample-declarative-ui/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..ee7e3ca
--- /dev/null
+++ b/sample-declarative-ui/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
\ No newline at end of file
diff --git a/sample-declarative-ui/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/sample-declarative-ui/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..fb88a39
--- /dev/null
+++ b/sample-declarative-ui/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,98 @@
+{
+ "images" : [
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "60x60"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "60x60"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "76x76"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "76x76"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "83.5x83.5"
+ },
+ {
+ "idiom" : "ios-marketing",
+ "scale" : "1x",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
\ No newline at end of file
diff --git a/sample-declarative-ui/iosApp/iosApp/Assets.xcassets/Contents.json b/sample-declarative-ui/iosApp/iosApp/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..4aa7c53
--- /dev/null
+++ b/sample-declarative-ui/iosApp/iosApp/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
\ No newline at end of file
diff --git a/sample-declarative-ui/iosApp/iosApp/ContentView.swift b/sample-declarative-ui/iosApp/iosApp/ContentView.swift
new file mode 100644
index 0000000..99a5c5d
--- /dev/null
+++ b/sample-declarative-ui/iosApp/iosApp/ContentView.swift
@@ -0,0 +1,21 @@
+import SwiftUI
+import MultiPlatformLibrary
+import Combine
+
+
+struct ContentView: View {
+ var body: some View {
+ LoginScreen(
+ viewModel: ListViewModel(
+ withInitialValue: false,
+ withError: false
+ )
+ )
+ }
+}
+
+struct ContentView_Previews: PreviewProvider {
+ static var previews: some View {
+ ContentView()
+ }
+}
diff --git a/sample-declarative-ui/iosApp/iosApp/Info.plist b/sample-declarative-ui/iosApp/iosApp/Info.plist
new file mode 100644
index 0000000..8044709
--- /dev/null
+++ b/sample-declarative-ui/iosApp/iosApp/Info.plist
@@ -0,0 +1,48 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ LSRequiresIPhoneOS
+
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+
+ UIRequiredDeviceCapabilities
+
+ armv7
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UILaunchScreen
+
+
+
\ No newline at end of file
diff --git a/sample-declarative-ui/iosApp/iosApp/ListBinding.swift b/sample-declarative-ui/iosApp/iosApp/ListBinding.swift
new file mode 100644
index 0000000..254e7b8
--- /dev/null
+++ b/sample-declarative-ui/iosApp/iosApp/ListBinding.swift
@@ -0,0 +1,105 @@
+//
+// LoginUI.swift
+// iosApp
+//
+// Created by mdubkov on 02.06.2022.
+//
+
+import SwiftUI
+import MultiPlatformLibrary
+import mokoMvvmFlowSwiftUI
+import Combine
+
+struct LoginScreen: View {
+ @StateObject private var viewModel: ListViewModel
+ @State private var state: ResourceStateKs = .loading
+
+ init(viewModel: ListViewModel) {
+ _viewModel = StateObject(wrappedValue: viewModel)
+ viewModel.start()
+ }
+
+ var body: some View {
+ LoginScreenState(
+ state: $state,
+ loadNextPage: { viewModel.onLoadNextPage() },
+ refresh: { return try await viewModel.onRefreshSuspend() },
+ onRetryPressed: { viewModel.onRetryPressed() }
+ ).onReceive(viewModel.stateKs) { state = $0 }
+ }
+}
+
+extension ListViewModel {
+ var stateKs: AnyPublisher, Never> {
+ createPublisher(state).map {
+ ResourceStateKs($0)
+ }.eraseToAnyPublisher()
+ }
+}
+
+private extension ResourceStateData where T == NSArray {
+ var dataKs: [ListUnitKs] {
+ self.data?.map { ListUnitKs($0 as! ListUnit) } ?? []
+ }
+}
+
+struct LoginScreenState: View {
+ @Binding var state: ResourceStateKs
+ let loadNextPage: () -> Void
+ let refresh: () async throws -> KotlinUnit
+ let onRetryPressed: () -> Void
+
+ var body: some View {
+ switch state {
+ case .empty:
+ Text("Empty")
+ case .loading:
+ ProgressView()
+ case .data(let data):
+ ListScreenBody(
+ listItems: data.dataKs,
+ loadNextPage: loadNextPage,
+ refresh: refresh
+ )
+ case .error(let error):
+ Text("Error: \(error.error ?? "unknown")")
+ Button {
+ onRetryPressed()
+ } label: {
+ Text("Retry")
+ }
+
+ }
+ }
+}
+
+struct LoginScreenStatePreview: PreviewProvider {
+ @State static var emptyState: ResourceStateKs = .empty
+ @State static var loadingState: ResourceStateKs = .loading
+ @State static var errorState: ResourceStateKs = .error(ResourceStateError(error: "Some Error"))
+
+ static var previewLambda: () async throws -> KotlinUnit = {
+ return KotlinUnit()
+ }
+
+ static var previews: some View {
+ LoginScreenState(
+ state: $emptyState,
+ loadNextPage: { },
+ refresh: previewLambda,
+ onRetryPressed: { }
+ )
+ LoginScreenState(
+ state: $loadingState,
+ loadNextPage: { },
+ refresh: previewLambda,
+ onRetryPressed: { }
+ )
+ LoginScreenState(
+ state: $loadingState,
+ loadNextPage: { },
+ refresh: previewLambda,
+ onRetryPressed: { }
+ )
+ }
+}
diff --git a/sample-declarative-ui/iosApp/iosApp/ListScreenBody.swift b/sample-declarative-ui/iosApp/iosApp/ListScreenBody.swift
new file mode 100644
index 0000000..9188be6
--- /dev/null
+++ b/sample-declarative-ui/iosApp/iosApp/ListScreenBody.swift
@@ -0,0 +1,82 @@
+//
+// LoginScreenBody.swift
+// iosApp
+//
+// Created by mdubkov on 02.06.2022.
+//
+
+import SwiftUI
+import MultiPlatformLibrary
+
+struct ListScreenBody: View {
+ let listItems: Array
+ var loadNextPage: () -> Void
+ var refresh: () async throws -> KotlinUnit
+
+ var body: some View {
+ List(listItems) { item in
+ VStack {
+ switch item {
+ case .productUnit(let listUnitProductUnit):
+ Text("Product #\(listUnitProductUnit.id)")
+ case .loading:
+ ProgressView()
+ .frame(height: 20)
+ }
+ }.onAppear {
+ // if you scrolled through 80% of the list
+ let index = listItems.firstIndex { $0.id == item.id }!
+ if Double(index) >= 0.8 * Double(listItems.count) {
+ loadNextPage()
+ }
+ }
+ }.listStyle(.grouped)
+ .refreshable {
+ let _ = try? await refresh()
+ }
+ }
+}
+
+extension ListUnitKs: Identifiable {
+ public var id: Int {
+ switch self {
+ case .productUnit(let listUnitProductUnit):
+ return Int(listUnitProductUnit.id)
+ case .loading:
+ return -1
+ }
+ }
+}
+
+struct ListScreenBody_Previews: PreviewProvider {
+ static let email: Array = [
+ .productUnit(
+ ListUnitProductUnit(
+ id: Int64(1),
+ title: "Milf"
+ )
+ ),
+ .productUnit(
+ ListUnitProductUnit(
+ id: Int64(2),
+ title: "Cookie"
+ )
+ ),
+ .productUnit(
+ ListUnitProductUnit(
+ id: Int64(3),
+ title: "Water"
+ )
+ ),
+ .loading
+ ]
+
+
+ static var previews: some View {
+ ListScreenBody(
+ listItems: email,
+ loadNextPage: { },
+ refresh: { return KotlinUnit() }
+ )
+ }
+}
diff --git a/sample-declarative-ui/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/sample-declarative-ui/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 0000000..4aa7c53
--- /dev/null
+++ b/sample-declarative-ui/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
\ No newline at end of file
diff --git a/sample-declarative-ui/iosApp/iosApp/fromMpp/dev_icerock_moko_mvvm-flow.swift b/sample-declarative-ui/iosApp/iosApp/fromMpp/dev_icerock_moko_mvvm-flow.swift
new file mode 100644
index 0000000..f2df171
--- /dev/null
+++ b/sample-declarative-ui/iosApp/iosApp/fromMpp/dev_icerock_moko_mvvm-flow.swift
@@ -0,0 +1,173 @@
+// This file automatically generated by MOKO KSwift (https://github.com/icerockdev/moko-kswift)
+//
+import Foundation
+import MultiPlatformLibrary
+import UIKit
+
+public extension MultiPlatformLibrary.DisposableHandle {
+ /**
+ * selector: PackageFunctionContext/dev.icerock.moko:mvvm-flow/dev.icerock.moko.mvvm.flow/Class(name=dev/icerock/moko/mvvm/flow/DisposableHandle)/plus/other:Class(name=dev/icerock/moko/mvvm/flow/DisposableHandle)
+ */
+ @discardableResult
+ public func plus(other: DisposableHandle) -> DisposableHandle {
+ return DisposableHandleKt.plus(self, other: other)
+ }
+}
+
+public extension UIKit.UIView {
+ /**
+ * selector: PackageFunctionContext/dev.icerock.moko:mvvm-flow/dev.icerock.moko.mvvm.flow.binding/Class(name=platform/UIKit/UIView)/bindBackgroundColor/flow:Class(name=dev/icerock/moko/mvvm/flow/CStateFlow),trueColor:Class(name=platform/UIKit/UIColor),falseColor:Class(name=platform/UIKit/UIColor)
+ */
+ @discardableResult
+ public func bindBackgroundColor(
+ flow: CStateFlow,
+ trueColor: UIColor,
+ falseColor: UIColor
+ ) -> DisposableHandle {
+ return UIViewBindingsKt.bindBackgroundColor(self, flow: flow, trueColor: trueColor, falseColor: falseColor)
+ }
+}
+
+public extension UIKit.UIControl {
+ /**
+ * selector: PackageFunctionContext/dev.icerock.moko:mvvm-flow/dev.icerock.moko.mvvm.flow.binding/Class(name=platform/UIKit/UIControl)/bindEnabled/flow:Class(name=dev/icerock/moko/mvvm/flow/CStateFlow)
+ */
+ @discardableResult
+ public func bindEnabled(flow: CStateFlow) -> DisposableHandle {
+ return UIControlBindingsKt.bindEnabled(self, flow: flow)
+ }
+}
+
+public extension UIKit.UIView {
+ /**
+ * selector: PackageFunctionContext/dev.icerock.moko:mvvm-flow/dev.icerock.moko.mvvm.flow.binding/Class(name=platform/UIKit/UIView)/bindFocus/flow:Class(name=dev/icerock/moko/mvvm/flow/CStateFlow)
+ */
+ @discardableResult
+ public func bindFocus(flow: CStateFlow) -> DisposableHandle {
+ return UIViewBindingsKt.bindFocus(self, flow: flow)
+ }
+}
+
+public extension UIKit.UIControl {
+ /**
+ * selector: PackageFunctionContext/dev.icerock.moko:mvvm-flow/dev.icerock.moko.mvvm.flow.binding/Class(name=platform/UIKit/UIControl)/bindFocusTwoWay/flow:Class(name=dev/icerock/moko/mvvm/flow/CMutableStateFlow)
+ */
+ @discardableResult
+ public func bindFocusTwoWay(flow: CMutableStateFlow) -> DisposableHandle {
+ return UIControlBindingsKt.bindFocusTwoWay(self, flow: flow)
+ }
+}
+
+public extension UIKit.UITextView {
+ /**
+ * selector: PackageFunctionContext/dev.icerock.moko:mvvm-flow/dev.icerock.moko.mvvm.flow.binding/Class(name=platform/UIKit/UITextView)/bindFocusTwoWay/flow:Class(name=dev/icerock/moko/mvvm/flow/CMutableStateFlow)
+ */
+ @discardableResult
+ public func bindFocusTwoWay(flow: CMutableStateFlow) -> DisposableHandle {
+ return UITextViewBindingsKt.bindFocusTwoWay(self, flow: flow)
+ }
+}
+
+public extension UIKit.UIView {
+ /**
+ * selector: PackageFunctionContext/dev.icerock.moko:mvvm-flow/dev.icerock.moko.mvvm.flow.binding/Class(name=platform/UIKit/UIView)/bindHidden/flow:Class(name=dev/icerock/moko/mvvm/flow/CStateFlow)
+ */
+ @discardableResult
+ public func bindHidden(flow: CStateFlow) -> DisposableHandle {
+ return UIViewBindingsKt.bindHidden(self, flow: flow)
+ }
+}
+
+public extension UIKit.UIButton {
+ /**
+ * selector: PackageFunctionContext/dev.icerock.moko:mvvm-flow/dev.icerock.moko.mvvm.flow.binding/Class(name=platform/UIKit/UIButton)/bindImage/flow:Class(name=dev/icerock/moko/mvvm/flow/CStateFlow),trueImage:Class(name=platform/UIKit/UIImage),falseImage:Class(name=platform/UIKit/UIImage)
+ */
+ @discardableResult
+ public func bindImage(
+ flow: CStateFlow,
+ trueImage: UIImage,
+ falseImage: UIImage
+ ) -> DisposableHandle {
+ return UIButtonBindingsKt.bindImage(self, flow: flow, trueImage: trueImage, falseImage: falseImage)
+ }
+}
+
+public extension UIKit.UISwitch {
+ /**
+ * selector: PackageFunctionContext/dev.icerock.moko:mvvm-flow/dev.icerock.moko.mvvm.flow.binding/Class(name=platform/UIKit/UISwitch)/bindSwitchOn/flow:Class(name=dev/icerock/moko/mvvm/flow/CStateFlow)