From 2a322d3fec9b00c4db49c9a123a4d34d58c07598 Mon Sep 17 00:00:00 2001 From: mdubkov Date: Fri, 3 Jun 2022 12:25:09 +0700 Subject: [PATCH 1/6] #39 add test, flow, core modules + up version --- gradle/libs.versions.toml | 8 +- paging-core/build.gradle.kts | 18 ++ .../src/androidMain/AndroidManifest.xml | 2 + .../dev/icerock/moko/paging/core/Identify.kt | 11 ++ .../paging/core}/LambdaPagedListDataSource.kt | 2 +- .../moko/paging/core}/PagedListDataSource.kt | 2 +- .../icerock/moko/paging/core/Pagination.kt | 42 +++++ .../moko/paging/core}/ReachEndNotifierList.kt | 2 +- paging-flow/build.gradle.kts | 24 +++ .../src/androidMain/AndroidManifest.xml | 2 + .../dev/icerock/moko/paging/flow/FlowExt.kt | 51 ++++++ .../icerock/moko/paging/flow/Pagination.kt | 160 +++++++++++++++++ .../moko/paging/flow/IntegrationTests.kt | 170 ++++++++++++++++++ .../moko/paging/flow/PaginationTest.kt | 137 ++++++++++++++ {paging => paging-livedata}/build.gradle.kts | 7 +- .../src/androidMain/AndroidManifest.xml | 2 + .../moko/paging/livedata}/LiveDataExt.kt | 4 +- .../moko/paging/livedata}/Pagination.kt | 69 ++----- .../moko/paging/livedata}/IntegrationTests.kt | 5 +- .../moko/paging/livedata}/PaginationTest.kt | 7 +- paging-state/build.gradle.kts | 14 ++ .../src/androidMain/AndroidManifest.xml | 2 + .../moko/paging/state/PagingDataState.kt | 20 +++ .../moko/paging/state/ResourceState.kt | 21 +++ paging-test/build.gradle.kts | 24 +++ .../src/androidMain/AndroidManifest.xml | 2 + .../moko/paging/test}/BaseTestsClass.kt | 2 +- .../dev/icerock/moko/paging/test}/UtilsJvm.kt | 2 +- .../moko/paging/test}/BaseTestsClass.kt | 2 +- .../moko/paging/test}/TestListDataSource.kt | 6 +- .../dev/icerock/moko/paging/test}/Utils.kt | 4 +- .../moko/paging/test}/BaseTestsClass.kt | 2 +- .../dev/icerock/moko/paging/test}/Utils.kt | 3 +- .../moko/paging/test}/BaseTestsClass.kt | 2 +- .../dev/icerock/moko/paging/test}/runTest.kt | 2 +- paging/src/androidMain/AndroidManifest.xml | 2 - paging/src/iosMain/kotlin/Dummy.kt | 8 - sample/mpp-library/build.gradle.kts | 2 +- .../com/icerockdev/library/ListViewModel.kt | 7 +- settings.gradle.kts | 6 +- 40 files changed, 765 insertions(+), 93 deletions(-) create mode 100644 paging-core/build.gradle.kts create mode 100755 paging-core/src/androidMain/AndroidManifest.xml create mode 100644 paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Identify.kt rename {paging/src/commonMain/kotlin/dev/icerock/moko/paging => paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core}/LambdaPagedListDataSource.kt (90%) rename {paging/src/commonMain/kotlin/dev/icerock/moko/paging => paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core}/PagedListDataSource.kt (84%) create mode 100644 paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Pagination.kt rename {paging/src/commonMain/kotlin/dev/icerock/moko/paging => paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core}/ReachEndNotifierList.kt (97%) create mode 100644 paging-flow/build.gradle.kts create mode 100755 paging-flow/src/androidMain/AndroidManifest.xml create mode 100644 paging-flow/src/commonMain/kotlin/dev/icerock/moko/paging/flow/FlowExt.kt create mode 100644 paging-flow/src/commonMain/kotlin/dev/icerock/moko/paging/flow/Pagination.kt create mode 100644 paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/IntegrationTests.kt create mode 100644 paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/PaginationTest.kt rename {paging => paging-livedata}/build.gradle.kts (65%) create mode 100755 paging-livedata/src/androidMain/AndroidManifest.xml rename {paging/src/commonMain/kotlin/dev/icerock/moko/paging => paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/livedata}/LiveDataExt.kt (81%) rename {paging/src/commonMain/kotlin/dev/icerock/moko/paging => paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/livedata}/Pagination.kt (78%) rename {paging/src/commonTest/kotlin/dev/icerock/moko/paging => paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata}/IntegrationTests.kt (96%) rename {paging/src/commonTest/kotlin/dev/icerock/moko/paging => paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata}/PaginationTest.kt (91%) create mode 100644 paging-state/build.gradle.kts create mode 100755 paging-state/src/androidMain/AndroidManifest.xml create mode 100644 paging-state/src/commonMain/kotlin/dev/icerock/moko/paging/state/PagingDataState.kt create mode 100644 paging-state/src/commonMain/kotlin/dev/icerock/moko/paging/state/ResourceState.kt create mode 100644 paging-test/build.gradle.kts create mode 100755 paging-test/src/androidMain/AndroidManifest.xml rename {paging/src/androidTest/kotlin/dev/icerock/moko/paging => paging-test/src/androidMain/kotlin/dev/icerock/moko/paging/test}/BaseTestsClass.kt (88%) rename {paging/src/androidTest/kotlin/dev/icerock/moko/paging => paging-test/src/androidMain/kotlin/dev/icerock/moko/paging/test}/UtilsJvm.kt (87%) rename {paging/src/commonTest/kotlin/dev/icerock/moko/paging => paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test}/BaseTestsClass.kt (79%) rename {paging/src/commonTest/kotlin/dev/icerock/moko/paging => paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test}/TestListDataSource.kt (71%) rename {paging/src/commonTest/kotlin/dev/icerock/moko/paging => paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test}/Utils.kt (81%) rename {paging/src/iosTest/kotlin/dev/icerock/moko/paging => paging-test/src/iosMain/kotlin/dev/icerock/moko/paging/test}/BaseTestsClass.kt (79%) rename {paging/src/iosTest/kotlin/dev/icerock/moko/paging => paging-test/src/iosMain/kotlin/dev/icerock/moko/paging/test}/Utils.kt (96%) rename {paging/src/jvmTest/kotlin/dev/icerock/moko/paging => paging-test/src/jvmMain/kotlin/dev/icerock/moko/paging/test}/BaseTestsClass.kt (79%) rename {paging/src/jvmTest/kotlin/dev/icerock/moko/paging => paging-test/src/jvmMain/kotlin/dev/icerock/moko/paging/test}/runTest.kt (81%) delete mode 100755 paging/src/androidMain/AndroidManifest.xml delete mode 100644 paging/src/iosMain/kotlin/Dummy.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1583b62..81842c8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,10 +7,10 @@ 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" +mokoPagingVersion = "0.8.0" [libraries] appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" } @@ -24,6 +24,8 @@ 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" } 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" } diff --git a/paging-core/build.gradle.kts b/paging-core/build.gradle.kts new file mode 100644 index 0000000..929f8ba --- /dev/null +++ b/paging-core/build.gradle.kts @@ -0,0 +1,18 @@ +/* + * 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") +} + +kotlin { + jvm() +} + +dependencies { + commonMainImplementation(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/core/Identify.kt b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Identify.kt new file mode 100644 index 0000000..3c4e1b7 --- /dev/null +++ b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Identify.kt @@ -0,0 +1,11 @@ +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-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Pagination.kt b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Pagination.kt new file mode 100644 index 0000000..9248aba --- /dev/null +++ b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Pagination.kt @@ -0,0 +1,42 @@ +package dev.icerock.moko.paging.core + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlin.coroutines.CoroutineContext + +abstract class Pagination( + parentScope: CoroutineScope, + protected val dataSource: PagedListDataSource, + protected val comparator: Comparator, + protected val nextPageListener: (Result>) -> Unit, + protected val refreshListener: (Result>) -> Unit, +) : CoroutineScope { + + override val coroutineContext: CoroutineContext = parentScope.coroutineContext + + protected var loadNextPageDeferred: Deferred>? = null + protected val listMutex = Mutex() + + fun loadFirstPage() { + launch { loadFirstPageSuspend() } + } + + fun loadNextPage() { + launch { loadNextPageSuspend() } + } + + fun refresh() { + launch { refreshSuspend() } + } + + fun setData(items: List?) { + launch { setDataSuspend(items) } + } + + abstract suspend fun loadFirstPageSuspend() + abstract suspend fun loadNextPageSuspend() + abstract suspend fun refreshSuspend() + abstract suspend fun setDataSuspend(items: List?) +} 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..d12ec62 --- /dev/null +++ b/paging-flow/build.gradle.kts @@ -0,0 +1,24 @@ +/* + * 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") +} + +kotlin { + jvm() +} + +dependencies { + commonMainImplementation(libs.coroutines) + commonMainApi(libs.mokoMvvmFlow) + commonMainApi(libs.mokoMvvmCore) + commonMainApi(libs.mokoMvvmState) + commonMainApi(projects.pagingCore) + + 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..8c3ea0e --- /dev/null +++ b/paging-flow/src/commonMain/kotlin/dev/icerock/moko/paging/flow/FlowExt.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2020 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> = combine(this, loading) { items, nextPageLoading -> + if (nextPageLoading) { + items + itemFactory() + } else { + items + } +}.stateIn(parentScope, SharingStarted.Lazily, emptyList()) + +fun Flow>.stateWithReachEndNotifier( + parentScope: CoroutineScope, + action: (Int) -> Unit +): StateFlow> = map { list -> + list.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..3348bcd --- /dev/null +++ b/paging-flow/src/commonMain/kotlin/dev/icerock/moko/paging/flow/Pagination.kt @@ -0,0 +1,160 @@ +/* + * Copyright 2020 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.ResourceState +import dev.icerock.moko.mvvm.asState +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.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +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 mStateStorage: MutableStateFlow, Throwable>> = + MutableStateFlow(initValue.asStateNullIsLoading()) + + val state: StateFlow, Throwable>> = mStateStorage + + private val mNextPageLoading: MutableStateFlow = MutableStateFlow(false) + val nextPageLoading: StateFlow get() = mNextPageLoading + + private val mEndOfList: MutableStateFlow = MutableStateFlow(false) + + private val mRefreshLoading: MutableStateFlow = MutableStateFlow(false) + val refreshLoading: StateFlow get() = mRefreshLoading + + override suspend fun loadFirstPageSuspend() { + loadNextPageDeferred?.cancel() + + listMutex.lock() + + mEndOfList.value = false + mNextPageLoading.value = false + mStateStorage.value = ResourceState.Loading() + + @Suppress("TooGenericExceptionCaught") + try { + val items: List = dataSource.loadPage(null) + mStateStorage.value = items.asState() + } catch (error: Exception) { + mStateStorage.value = ResourceState.Failed(error) + } + listMutex.unlock() + } + + @Suppress("ReturnCount") + override suspend fun loadNextPageSuspend() { + if (mNextPageLoading.value) return + if (mRefreshLoading.value) return + if (mEndOfList.value) return + + listMutex.lock() + + mNextPageLoading.value = true + + @Suppress("TooGenericExceptionCaught") + try { + loadNextPageDeferred = this.async { + val currentList = mStateStorage.value.dataValue() + ?: throw IllegalStateException("Try to load next page when list is empty") + // load next page items + val items = dataSource.loadPage(currentList) + // remove already exist items + val newItems = items.filter { item -> + val existsItem = currentList.firstOrNull { comparator.compare(item, it) == 0 } + existsItem == null + } + // append new items to current list + val newList = currentList.plus(newItems) + // mark end of list if no new items + if (newItems.isEmpty()) { + mEndOfList.value = true + } else { + // save + mStateStorage.value = newList.asState() + } + newList + } + val newList = loadNextPageDeferred!!.await() + + // flag + mNextPageLoading.value = false + // notify + nextPageListener(Result.success(newList)) + } catch (error: Exception) { + // flag + mNextPageLoading.value = false + // notify + nextPageListener(Result.failure(error)) + } + listMutex.unlock() + } + + override suspend fun refreshSuspend() { + loadNextPageDeferred?.cancel() + listMutex.lock() + + if (mRefreshLoading.value) { + listMutex.unlock() + return + } + if (mNextPageLoading.value) { + listMutex.unlock() + return + } + + mRefreshLoading.value = true + + @Suppress("TooGenericExceptionCaught") + try { + // load first page items + val items = dataSource.loadPage(null) + // save + mStateStorage.value = items.asState() + // flag + mEndOfList.value = false + mRefreshLoading.value = false + // notify + refreshListener(Result.success(items)) + } catch (error: Exception) { + // flag + mRefreshLoading.value = false + // notify + refreshListener(Result.failure(error)) + } + listMutex.unlock() + } + + override suspend fun setDataSuspend(items: List?) { + listMutex.lock() + mStateStorage.value = items.asStateNullIsEmpty() + mEndOfList.value = false + listMutex.unlock() + } +} + +fun List?.asStateNullIsEmpty() = asState { + ResourceState.Empty, E>() +} + +fun List?.asStateNullIsLoading() = asState { + ResourceState.Loading, E>() +} diff --git a/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/IntegrationTests.kt b/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/IntegrationTests.kt new file mode 100644 index 0000000..af3ec37 --- /dev/null +++ b/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/IntegrationTests.kt @@ -0,0 +1,170 @@ +/* + * Copyright 2020 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.LambdaPagedListDataSource +import dev.icerock.moko.paging.test.BaseTestsClass +import dev.icerock.moko.paging.test.runTest +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.http.fullPath +import kotlinx.coroutines.async +import kotlinx.coroutines.cancel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.test.Test + +class IntegrationTests : BaseTestsClass() { + private val httpClient = HttpClient(MockEngine) { + engine { + addHandler { request -> + when (request.url.toString()) { + "http://api.icndb.com/jokes/random" -> { + delay(200) + respondOk(""" + { + "type": "success", + "value": { + "id": 318, + "joke": "If you work in an office with Chuck Norris, don't ask him for his three-hole-punch.", + "categories": [] + } + } + """.trimIndent()) + } + else -> error("Unhandled ${request.url.fullPath}") + } + } + } + } + + @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 = { } + ) + + for (i in 0..10) { + println("--- ITERATION $i START ---") + println("start load first page") + pagination.loadFirstPageSuspend() + println("end load first page") + + (0..3).flatMap { + listOf( + async { + println("--> $it refresh start") + pagination.refreshSuspend() + println("<-- $it refresh end") + }, + async { + println("--> $it load next page start") + pagination.loadNextPageSuspend() + println("<-- $it load next page end") + } + ) + }.forEach { it.await() } + } + } + + @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 = { a, b -> a.compareTo(b) }, + nextPageListener = { }, + refreshListener = { } + ) + + for (i in 0..10) { + println("--- ITERATION $i START ---") + println("start load first page") + pagination.loadFirstPageSuspend() + println("end load first page") + + (0..1).flatMap { + listOf( + async { + println("--> $it refresh start") + pagination.refreshSuspend() + println("<-- $it refresh end") + }, + async { + println("--> $it load next page start") + pagination.loadNextPageSuspend() + println("<-- $it load next page end") + }, + async { + println("--> $it set data start") + val data = pagination.state.value.dataValue().orEmpty() + val newData = data.plus("new item") + pagination.setDataSuspend(newData) + println("--> $it set data end") + } + ) + }.forEach { it.await() } + } + } + + @Test + 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 = { } + ) + + launch { + println("start load") + pagination.loadFirstPageSuspend() + println("end load") + } + + delay(50) + println("cancel scope") + cancel() + } + } + + println(exc) + } +} \ No newline at end of file diff --git a/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/PaginationTest.kt b/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/PaginationTest.kt new file mode 100644 index 0000000..7250ee6 --- /dev/null +++ b/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/PaginationTest.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2020 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.LambdaPagedListDataSource +import dev.icerock.moko.paging.test.BaseTestsClass +import dev.icerock.moko.paging.test.TestListDataSource +import dev.icerock.moko.paging.test.compareWith +import dev.icerock.moko.paging.test.runTest +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertTrue + +class PaginationTest : BaseTestsClass() { + + var paginationDataSource = TestListDataSource(3, 5) + + val itemsComparator = Comparator { a: Int, b: Int -> + a - b + } + + @BeforeTest + fun setup() { + paginationDataSource = TestListDataSource(3, 5) + } + + @Test + fun `load first page`() = runTest { + val pagination = createPagination() + + pagination.loadFirstPageSuspend() + + assertTrue { + pagination.state.value.isSuccess() + } + assertTrue { + pagination.state.value.dataValue()!!.compareWith(listOf(0, 1, 2)) + } + } + + @Test + fun `load next page`() = runTest { + val pagination = createPagination() + + pagination.loadFirstPageSuspend() + pagination.loadNextPageSuspend() + + assertTrue { + pagination.state.value.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)) + } + } + + @Test + fun `refresh pagination`() = runTest { + val pagination = createPagination() + + pagination.loadFirstPageSuspend() + pagination.loadNextPageSuspend() + pagination.refreshSuspend() + + assertTrue { + pagination.state.value.dataValue()!!.compareWith(listOf(0, 1, 2)) + } + } + + @Test + fun `set data`() = runTest { + val pagination = createPagination() + + pagination.loadFirstPageSuspend() + pagination.loadNextPageSuspend() + + val setList = listOf(5, 2, 3, 1, 4) + pagination.setDataSuspend(setList) + + assertTrue { + pagination.state.value.dataValue()!!.compareWith(setList) + } + } + + @Test + fun `double refresh`() = runTest { + var counter = 0 + val pagination = Pagination( + parentScope = 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") + pagination.loadFirstPageSuspend() + println("end load first page") + + println("start double refresh") + val r1 = async { + pagination.refreshSuspend() + println("first refresh end") + } + val r2 = async { + pagination.refreshSuspend() + println("second refresh end") + } + + 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/build.gradle.kts b/paging-livedata/build.gradle.kts similarity index 65% rename from paging/build.gradle.kts rename to paging-livedata/build.gradle.kts index bb34262..4e22ef0 100644 --- a/paging/build.gradle.kts +++ b/paging-livedata/build.gradle.kts @@ -17,10 +17,7 @@ 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/src/commonMain/kotlin/dev/icerock/moko/paging/LiveDataExt.kt b/paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/livedata/LiveDataExt.kt similarity index 81% 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..ac9978d 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 @@ -2,11 +2,13 @@ * 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.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/src/commonMain/kotlin/dev/icerock/moko/paging/Pagination.kt b/paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/livedata/Pagination.kt similarity index 78% rename from paging/src/commonMain/kotlin/dev/icerock/moko/paging/Pagination.kt rename to paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/livedata/Pagination.kt index f596fe3..794cc7c 100644 --- a/paging/src/commonMain/kotlin/dev/icerock/moko/paging/Pagination.kt +++ b/paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/livedata/Pagination.kt @@ -2,27 +2,32 @@ * 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.livedata 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 dev.icerock.moko.paging.core.PagedListDataSource +import dev.icerock.moko.paging.core.Pagination import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred import kotlinx.coroutines.async -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex import kotlin.coroutines.CoroutineContext class Pagination( parentScope: CoroutineScope, - private val dataSource: PagedListDataSource, - private val comparator: Comparator, - private val nextPageListener: (Result>) -> Unit, - private val refreshListener: (Result>) -> Unit, + dataSource: PagedListDataSource, + comparator: Comparator, + nextPageListener: (Result>) -> Unit, + refreshListener: (Result>) -> Unit, initValue: List? = null -) : CoroutineScope { +) : Pagination( + parentScope = parentScope, + dataSource = dataSource, + comparator = comparator, + nextPageListener = nextPageListener, + refreshListener = refreshListener, +) { override val coroutineContext: CoroutineContext = parentScope.coroutineContext @@ -39,17 +44,7 @@ class Pagination( private val mRefreshLoading = MutableLiveData(false) val refreshLoading = mRefreshLoading.readOnly() - private val listMutex = Mutex() - - private var loadNextPageDeferred: Deferred>? = null - - fun loadFirstPage() { - launch { - loadFirstPageSuspend() - } - } - - suspend fun loadFirstPageSuspend() { + override suspend fun loadFirstPageSuspend() { loadNextPageDeferred?.cancel() listMutex.lock() @@ -68,14 +63,8 @@ class Pagination( listMutex.unlock() } - fun loadNextPage() { - launch { - loadNextPageSuspend() - } - } - @Suppress("ReturnCount") - suspend fun loadNextPageSuspend() { + override suspend fun loadNextPageSuspend() { if (mNextPageLoading.value) return if (mRefreshLoading.value) return if (mEndOfList.value) return @@ -122,13 +111,7 @@ class Pagination( listMutex.unlock() } - fun refresh() { - launch { - refreshSuspend() - } - } - - suspend fun refreshSuspend() { + override suspend fun refreshSuspend() { loadNextPageDeferred?.cancel() listMutex.lock() @@ -163,13 +146,7 @@ class Pagination( listMutex.unlock() } - fun setData(items: List?) { - launch { - setDataSuspend(items) - } - } - - suspend fun setDataSuspend(items: List?) { + override suspend fun setDataSuspend(items: List?) { listMutex.lock() mStateStorage.value = items.asStateNullIsEmpty() mEndOfList.value = false @@ -184,13 +161,3 @@ fun List?.asStateNullIsEmpty() = asState { 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/commonTest/kotlin/dev/icerock/moko/paging/IntegrationTests.kt b/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/IntegrationTests.kt similarity index 96% rename from paging/src/commonTest/kotlin/dev/icerock/moko/paging/IntegrationTests.kt rename to paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/IntegrationTests.kt index 13e40f8..25e7a23 100644 --- a/paging/src/commonTest/kotlin/dev/icerock/moko/paging/IntegrationTests.kt +++ b/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/IntegrationTests.kt @@ -2,8 +2,11 @@ * 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.livedata +import dev.icerock.moko.paging.core.LambdaPagedListDataSource +import dev.icerock.moko.paging.test.BaseTestsClass +import dev.icerock.moko.paging.test.runTest import io.ktor.client.HttpClient import io.ktor.client.engine.mock.MockEngine import io.ktor.client.engine.mock.respondOk diff --git a/paging/src/commonTest/kotlin/dev/icerock/moko/paging/PaginationTest.kt b/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/PaginationTest.kt similarity index 91% rename from paging/src/commonTest/kotlin/dev/icerock/moko/paging/PaginationTest.kt rename to paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/PaginationTest.kt index bedb7cb..e106879 100644 --- a/paging/src/commonTest/kotlin/dev/icerock/moko/paging/PaginationTest.kt +++ b/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/PaginationTest.kt @@ -2,8 +2,13 @@ * 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.livedata +import dev.icerock.moko.paging.core.LambdaPagedListDataSource +import dev.icerock.moko.paging.test.BaseTestsClass +import dev.icerock.moko.paging.test.TestListDataSource +import dev.icerock.moko.paging.test.compareWith +import dev.icerock.moko.paging.test.runTest import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.delay diff --git a/paging-state/build.gradle.kts b/paging-state/build.gradle.kts new file mode 100644 index 0000000..9dbf798 --- /dev/null +++ b/paging-state/build.gradle.kts @@ -0,0 +1,14 @@ +/* + * 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") +} + +kotlin { + jvm() +} 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/PagingDataState.kt b/paging-state/src/commonMain/kotlin/dev/icerock/moko/paging/state/PagingDataState.kt new file mode 100644 index 0000000..67ac4e2 --- /dev/null +++ b/paging-state/src/commonMain/kotlin/dev/icerock/moko/paging/state/PagingDataState.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.paging.state + +sealed interface PagingDataState { + val items: List + data class Normal(override val items: List) : PagingDataState + data class Refresh(override val items: List) : PagingDataState + data class LoadNextPage(override val items: List) : PagingDataState + data class End(override val items: List) : PagingDataState + + val isNormal: Boolean get() = this is Normal<*> + val isRefresh: Boolean get() = this is Refresh<*> + val isLoadingNextPage: Boolean get() = this is LoadNextPage<*> + val isEnd: Boolean get() = this is End<*> + + val isLoading: Boolean get() = !isRefresh +} 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..b298585 --- /dev/null +++ b/paging-state/src/commonMain/kotlin/dev/icerock/moko/paging/state/ResourceState.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2020 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-test/build.gradle.kts b/paging-test/build.gradle.kts new file mode 100644 index 0000000..d95374e --- /dev/null +++ b/paging-test/build.gradle.kts @@ -0,0 +1,24 @@ +/* + * 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") +} + +kotlin { + jvm() +} + +dependencies { + commonMainApi(libs.coroutines) + commonMainApi(projects.pagingCore) + commonMainApi(libs.kotlinTestJUnit) + androidMainApi(libs.androidCoreTesting) + commonMainApi(libs.ktorClient) + commonMainApi(libs.ktorClientMock) + iosX64MainApi(libs.coroutines) +} \ No newline at end of file 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/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 96% 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..86cdc58 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 diff --git a/paging/src/jvmTest/kotlin/dev/icerock/moko/paging/BaseTestsClass.kt b/paging-test/src/jvmMain/kotlin/dev/icerock/moko/paging/test/BaseTestsClass.kt similarity index 79% rename from paging/src/jvmTest/kotlin/dev/icerock/moko/paging/BaseTestsClass.kt rename to paging-test/src/jvmMain/kotlin/dev/icerock/moko/paging/test/BaseTestsClass.kt index 1871835..30d8fe6 100644 --- a/paging/src/jvmTest/kotlin/dev/icerock/moko/paging/BaseTestsClass.kt +++ b/paging-test/src/jvmMain/kotlin/dev/icerock/moko/paging/test/BaseTestsClass.kt @@ -2,6 +2,6 @@ * 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 actual open class BaseTestsClass diff --git a/paging/src/jvmTest/kotlin/dev/icerock/moko/paging/runTest.kt b/paging-test/src/jvmMain/kotlin/dev/icerock/moko/paging/test/runTest.kt similarity index 81% rename from paging/src/jvmTest/kotlin/dev/icerock/moko/paging/runTest.kt rename to paging-test/src/jvmMain/kotlin/dev/icerock/moko/paging/test/runTest.kt index c6437bb..5af35c8 100644 --- a/paging/src/jvmTest/kotlin/dev/icerock/moko/paging/runTest.kt +++ b/paging-test/src/jvmMain/kotlin/dev/icerock/moko/paging/test/runTest.kt @@ -1,4 +1,4 @@ -package dev.icerock.moko.paging +package dev.icerock.moko.paging.test import kotlinx.coroutines.CoroutineScope 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/sample/mpp-library/build.gradle.kts b/sample/mpp-library/build.gradle.kts index 511fa4e..7cff14d 100644 --- a/sample/mpp-library/build.gradle.kts +++ b/sample/mpp-library/build.gradle.kts @@ -11,7 +11,7 @@ plugins { dependencies { commonMainImplementation(libs.coroutines) - commonMainApi(projects.paging) + commonMainApi(projects.pagingLivedata) commonMainApi(libs.mokoUnits) commonMainApi(libs.mokoMvvmLiveData) commonMainApi(libs.mokoMvvmState) diff --git a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/ListViewModel.kt b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/ListViewModel.kt index 452bb9b..f50f251 100644 --- a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/ListViewModel.kt +++ b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/ListViewModel.kt @@ -11,10 +11,9 @@ import dev.icerock.moko.mvvm.livedata.errorTransform import dev.icerock.moko.mvvm.livedata.map import dev.icerock.moko.mvvm.livedata.mediatorOf import dev.icerock.moko.mvvm.viewmodel.ViewModel -import dev.icerock.moko.paging.IdComparator -import dev.icerock.moko.paging.IdEntity -import dev.icerock.moko.paging.LambdaPagedListDataSource -import dev.icerock.moko.paging.Pagination +import dev.icerock.moko.paging.livedata.IdComparator +import dev.icerock.moko.paging.livedata.IdEntity +import dev.icerock.moko.paging.livedata.Pagination import dev.icerock.moko.units.TableUnitItem import kotlinx.coroutines.delay diff --git a/settings.gradle.kts b/settings.gradle.kts index 65f6bfe..d6a75ec 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,6 +24,10 @@ gradleEnterprise { } } -include(":paging") +include(":paging-core") +include(":paging-flow") +include(":paging-livedata") +include(":paging-state") +include(":paging-test") include(":sample:android-app") include(":sample:mpp-library") From a2e94d22b7a53184fd73f4730f7bfa25a2b9d384 Mon Sep 17 00:00:00 2001 From: Aleksey Mikhailov Date: Fri, 3 Jun 2022 12:46:21 +0700 Subject: [PATCH 2/6] add copyright settings --- .idea/copyright/IceRock.xml | 6 ++++++ .idea/copyright/profiles_settings.xml | 7 +++++++ 2 files changed, 13 insertions(+) create mode 100644 .idea/copyright/IceRock.xml create mode 100644 .idea/copyright/profiles_settings.xml 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 From 2f149bb3c4c34f1b6f2bc3f0fcd62b74da6856cf Mon Sep 17 00:00:00 2001 From: Aleksey Mikhailov Date: Fri, 3 Jun 2022 12:51:53 +0700 Subject: [PATCH 3/6] copyright fixes --- paging-core/build.gradle.kts | 2 +- .../kotlin/dev/icerock/moko/paging/core/Identify.kt | 4 ++++ .../kotlin/dev/icerock/moko/paging/core/Pagination.kt | 4 ++++ paging-flow/build.gradle.kts | 6 +++--- .../kotlin/dev/icerock/moko/paging/flow/FlowExt.kt | 2 +- .../kotlin/dev/icerock/moko/paging/flow/Pagination.kt | 2 +- .../kotlin/dev/icerock/moko/paging/flow/IntegrationTests.kt | 2 +- .../kotlin/dev/icerock/moko/paging/flow/PaginationTest.kt | 2 +- paging-livedata/build.gradle.kts | 3 ++- .../kotlin/dev/icerock/moko/paging/livedata/LiveDataExt.kt | 2 +- .../kotlin/dev/icerock/moko/paging/livedata/Pagination.kt | 2 +- .../dev/icerock/moko/paging/livedata/IntegrationTests.kt | 2 +- .../dev/icerock/moko/paging/livedata/PaginationTest.kt | 2 +- paging-state/build.gradle.kts | 2 +- .../kotlin/dev/icerock/moko/paging/state/PagingDataState.kt | 2 +- .../kotlin/dev/icerock/moko/paging/state/ResourceState.kt | 2 +- 16 files changed, 25 insertions(+), 16 deletions(-) diff --git a/paging-core/build.gradle.kts b/paging-core/build.gradle.kts index 929f8ba..0896c7e 100644 --- a/paging-core/build.gradle.kts +++ b/paging-core/build.gradle.kts @@ -1,5 +1,5 @@ /* - * 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. */ plugins { 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 index 3c4e1b7..2201ea5 100644 --- 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 @@ -1,3 +1,7 @@ +/* + * 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 { diff --git a/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Pagination.kt b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Pagination.kt index 9248aba..43fa8b3 100644 --- a/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Pagination.kt +++ b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Pagination.kt @@ -1,3 +1,7 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + package dev.icerock.moko.paging.core import kotlinx.coroutines.CoroutineScope diff --git a/paging-flow/build.gradle.kts b/paging-flow/build.gradle.kts index d12ec62..260249e 100644 --- a/paging-flow/build.gradle.kts +++ b/paging-flow/build.gradle.kts @@ -1,5 +1,5 @@ /* - * 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. */ plugins { @@ -15,10 +15,10 @@ kotlin { dependencies { commonMainImplementation(libs.coroutines) + + commonMainApi(projects.pagingCore) commonMainApi(libs.mokoMvvmFlow) - commonMainApi(libs.mokoMvvmCore) commonMainApi(libs.mokoMvvmState) - commonMainApi(projects.pagingCore) commonTestImplementation(projects.pagingTest) } 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 index 8c3ea0e..3fc5619 100644 --- 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 @@ -1,5 +1,5 @@ /* - * 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.flow 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 index 3348bcd..6c3a41f 100644 --- 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 @@ -1,5 +1,5 @@ /* - * 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.flow diff --git a/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/IntegrationTests.kt b/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/IntegrationTests.kt index af3ec37..4dbbffc 100644 --- a/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/IntegrationTests.kt +++ b/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/IntegrationTests.kt @@ -1,5 +1,5 @@ /* - * 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.flow diff --git a/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/PaginationTest.kt b/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/PaginationTest.kt index 7250ee6..bf8b91e 100644 --- a/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/PaginationTest.kt +++ b/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/PaginationTest.kt @@ -1,5 +1,5 @@ /* - * 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.flow diff --git a/paging-livedata/build.gradle.kts b/paging-livedata/build.gradle.kts index 4e22ef0..d2e485e 100644 --- a/paging-livedata/build.gradle.kts +++ b/paging-livedata/build.gradle.kts @@ -15,9 +15,10 @@ kotlin { dependencies { commonMainImplementation(libs.coroutines) + + commonMainApi(projects.pagingCore) commonMainApi(libs.mokoMvvmLiveData) commonMainApi(libs.mokoMvvmState) - commonMainApi(projects.pagingCore) commonTestImplementation(projects.pagingTest) } diff --git a/paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/livedata/LiveDataExt.kt b/paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/livedata/LiveDataExt.kt index ac9978d..3260cdc 100644 --- a/paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/livedata/LiveDataExt.kt +++ b/paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/livedata/LiveDataExt.kt @@ -1,5 +1,5 @@ /* - * 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.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 index 794cc7c..9d4dbea 100644 --- 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 @@ -1,5 +1,5 @@ /* - * 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.livedata diff --git a/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/IntegrationTests.kt b/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/IntegrationTests.kt index 25e7a23..c0cfb94 100644 --- a/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/IntegrationTests.kt +++ b/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/IntegrationTests.kt @@ -1,5 +1,5 @@ /* - * 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.livedata diff --git a/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/PaginationTest.kt b/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/PaginationTest.kt index e106879..568caf3 100644 --- a/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/PaginationTest.kt +++ b/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/PaginationTest.kt @@ -1,5 +1,5 @@ /* - * 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.livedata diff --git a/paging-state/build.gradle.kts b/paging-state/build.gradle.kts index 9dbf798..22ef75b 100644 --- a/paging-state/build.gradle.kts +++ b/paging-state/build.gradle.kts @@ -1,5 +1,5 @@ /* - * 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. */ plugins { diff --git a/paging-state/src/commonMain/kotlin/dev/icerock/moko/paging/state/PagingDataState.kt b/paging-state/src/commonMain/kotlin/dev/icerock/moko/paging/state/PagingDataState.kt index 67ac4e2..8c9f288 100644 --- a/paging-state/src/commonMain/kotlin/dev/icerock/moko/paging/state/PagingDataState.kt +++ b/paging-state/src/commonMain/kotlin/dev/icerock/moko/paging/state/PagingDataState.kt @@ -1,5 +1,5 @@ /* - * 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.state 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 index b298585..48e4747 100644 --- 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 @@ -1,5 +1,5 @@ /* - * 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.state From b6d6245bbbee2507ac92669141f64aeff01c09c1 Mon Sep 17 00:00:00 2001 From: mdubkov Date: Fri, 3 Jun 2022 22:32:40 +0700 Subject: [PATCH 4/6] #39 create abstract --- paging-core/build.gradle.kts | 2 +- .../icerock/moko/paging/core/Pagination.kt | 143 +++++++++++++++-- paging-flow/build.gradle.kts | 5 +- .../dev/icerock/moko/paging/flow/FlowExt.kt | 23 +-- .../icerock/moko/paging/flow/Pagination.kt | 147 ++++-------------- .../moko/paging/flow/PaginationTest.kt | 2 +- paging-livedata/build.gradle.kts | 1 - .../moko/paging/livedata/LiveDataExt.kt | 2 +- .../moko/paging/livedata/Pagination.kt | 138 +++------------- .../moko/paging/state/PagingDataState.kt | 20 --- .../moko/paging/state/ResourceState.kt | 2 +- .../moko/paging/state/ResourceStateExt.kt | 29 ++++ paging-test/build.gradle.kts | 5 +- .../com/icerockdev/library/ListViewModel.kt | 5 +- 14 files changed, 242 insertions(+), 282 deletions(-) delete mode 100644 paging-state/src/commonMain/kotlin/dev/icerock/moko/paging/state/PagingDataState.kt create mode 100644 paging-state/src/commonMain/kotlin/dev/icerock/moko/paging/state/ResourceStateExt.kt diff --git a/paging-core/build.gradle.kts b/paging-core/build.gradle.kts index 929f8ba..e644c9c 100644 --- a/paging-core/build.gradle.kts +++ b/paging-core/build.gradle.kts @@ -14,5 +14,5 @@ kotlin { } dependencies { - commonMainImplementation(libs.coroutines) + commonMainApi(libs.coroutines) } diff --git a/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Pagination.kt b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Pagination.kt index 9248aba..e58a8d5 100644 --- a/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Pagination.kt +++ b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Pagination.kt @@ -1,23 +1,33 @@ +/* + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + package dev.icerock.moko.paging.core 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 abstract class Pagination( parentScope: CoroutineScope, - protected val dataSource: PagedListDataSource, - protected val comparator: Comparator, - protected val nextPageListener: (Result>) -> Unit, - protected val refreshListener: (Result>) -> Unit, + private val dataSource: PagedListDataSource, + private val comparator: Comparator, + private val nextPageListener: (Result>) -> Unit, + private val refreshListener: (Result>) -> Unit, ) : CoroutineScope { override val coroutineContext: CoroutineContext = parentScope.coroutineContext - protected var loadNextPageDeferred: Deferred>? = null - protected val listMutex = Mutex() + protected val mNextPageLoading: MutableStateFlow = MutableStateFlow(false) + protected val mRefreshLoading: MutableStateFlow = MutableStateFlow(false) + private val mEndOfList: MutableStateFlow = MutableStateFlow(false) + + private var loadNextPageDeferred: Deferred>? = null + private val listMutex = Mutex() fun loadFirstPage() { launch { loadFirstPageSuspend() } @@ -35,8 +45,121 @@ abstract class Pagination( launch { setDataSuspend(items) } } - abstract suspend fun loadFirstPageSuspend() - abstract suspend fun loadNextPageSuspend() - abstract suspend fun refreshSuspend() - abstract suspend fun setDataSuspend(items: List?) + protected 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() + + listMutex.lock() + + mEndOfList.value = false + mNextPageLoading.value = false + setupLoadingState() + + @Suppress("TooGenericExceptionCaught") + try { + val items: List = dataSource.loadPage(null) + saveState(items) + } catch (error: Exception) { + saveErrorState(error) + } + listMutex.unlock() + } + + @Suppress("ReturnCount") + suspend fun loadNextPageSuspend() { + if (mNextPageLoading.value) return + if (mRefreshLoading.value) return + if (mEndOfList.value) return + + listMutex.lock() + + mNextPageLoading.value = true + + @Suppress("TooGenericExceptionCaught") + try { + loadNextPageDeferred = this.async { + val currentList = dataValue() + ?: throw IllegalStateException("Try to load next page when list is empty") + // load next page items + val items = dataSource.loadPage(currentList) + // remove already exist items + val newItems = items.filter { item -> + val existsItem = currentList.firstOrNull { comparator.compare(item, it) == 0 } + existsItem == null + } + // append new items to current list + val newList = currentList.plus(newItems) + // mark end of list if no new items + if (newItems.isEmpty()) { + mEndOfList.value = true + } else { + // save + saveState(newList) + } + newList + } + val newList = loadNextPageDeferred!!.await() + + // flag + mNextPageLoading.value = false + // notify + nextPageListener(Result.success(newList)) + } catch (error: Exception) { + // flag + mNextPageLoading.value = false + // notify + nextPageListener(Result.failure(error)) + } + listMutex.unlock() + } + + suspend fun refreshSuspend() { + loadNextPageDeferred?.cancel() + listMutex.lock() + + if (mRefreshLoading.value) { + listMutex.unlock() + return + } + if (mNextPageLoading.value) { + listMutex.unlock() + return + } + + mRefreshLoading.value = true + + @Suppress("TooGenericExceptionCaught") + try { + // load first page items + val items = dataSource.loadPage(null) + // save + saveState(items) + // flag + mEndOfList.value = false + mRefreshLoading.value = false + // notify + refreshListener(Result.success(items)) + } catch (error: Exception) { + // flag + mRefreshLoading.value = false + // notify + refreshListener(Result.failure(error)) + } + listMutex.unlock() + } + + suspend fun setDataSuspend(items: List?) { + listMutex.lock() + saveStateNullIsEmpty(items) + mEndOfList.value = false + listMutex.unlock() + } } + + diff --git a/paging-flow/build.gradle.kts b/paging-flow/build.gradle.kts index d12ec62..1252c9a 100644 --- a/paging-flow/build.gradle.kts +++ b/paging-flow/build.gradle.kts @@ -1,5 +1,5 @@ /* - * 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. */ plugins { @@ -14,11 +14,10 @@ kotlin { } dependencies { - commonMainImplementation(libs.coroutines) commonMainApi(libs.mokoMvvmFlow) commonMainApi(libs.mokoMvvmCore) - commonMainApi(libs.mokoMvvmState) commonMainApi(projects.pagingCore) + commonMainApi(projects.pagingState) commonTestImplementation(projects.pagingTest) } 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 index 8c3ea0e..340b95f 100644 --- 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 @@ -1,5 +1,5 @@ /* - * 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.flow @@ -35,17 +35,18 @@ fun Flow>.stateWithLoadingItem( parentScope: CoroutineScope, loading: Flow, itemFactory: () -> T -): StateFlow> = combine(this, loading) { items, nextPageLoading -> - if (nextPageLoading) { - items + itemFactory() - } else { - items - } -}.stateIn(parentScope, SharingStarted.Lazily, emptyList()) +): StateFlow> = this.withLoadingItem( + loading = loading, + itemFactory = itemFactory +).stateIn(parentScope, SharingStarted.Lazily, emptyList()) fun Flow>.stateWithReachEndNotifier( parentScope: CoroutineScope, action: (Int) -> Unit -): StateFlow> = map { list -> - list.withReachEndNotifier(action) -}.stateIn(parentScope, SharingStarted.Lazily, ReachEndNotifierList(emptyList(), action)) +): 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 index 3348bcd..7469a07 100644 --- 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 @@ -1,18 +1,19 @@ /* - * 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.flow -import dev.icerock.moko.mvvm.ResourceState -import dev.icerock.moko.mvvm.asState 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.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +typealias PaginationState = ResourceState, Throwable> + class Pagination( parentScope: CoroutineScope, dataSource: PagedListDataSource, @@ -25,136 +26,50 @@ class Pagination( dataSource = dataSource, comparator = comparator, nextPageListener = nextPageListener, - refreshListener = refreshListener + refreshListener = refreshListener, ) { - private val mStateStorage: MutableStateFlow, Throwable>> = + private val _state: MutableStateFlow> = MutableStateFlow(initValue.asStateNullIsLoading()) - val state: StateFlow, Throwable>> = mStateStorage - - private val mNextPageLoading: MutableStateFlow = MutableStateFlow(false) - val nextPageLoading: StateFlow get() = mNextPageLoading + val state: StateFlow, Throwable>> + get() = _state - private val mEndOfList: MutableStateFlow = MutableStateFlow(false) + val refreshLoading: StateFlow + get() = mRefreshLoading - private val mRefreshLoading: MutableStateFlow = MutableStateFlow(false) - val refreshLoading: StateFlow get() = mRefreshLoading + val nextPageLoading: StateFlow + get() = mNextPageLoading - override suspend fun loadFirstPageSuspend() { - loadNextPageDeferred?.cancel() + override fun dataValue(): List? = + _state.value.dataValue() - listMutex.lock() - mEndOfList.value = false - mNextPageLoading.value = false - mStateStorage.value = ResourceState.Loading() + override fun saveState(items: List) { + _state.value = items.asState() + } - @Suppress("TooGenericExceptionCaught") - try { - val items: List = dataSource.loadPage(null) - mStateStorage.value = items.asState() - } catch (error: Exception) { - mStateStorage.value = ResourceState.Failed(error) - } - listMutex.unlock() + override fun saveStateNullIsEmpty(items: List?) { + _state.value = items.asStateNullIsEmpty() } - @Suppress("ReturnCount") - override suspend fun loadNextPageSuspend() { - if (mNextPageLoading.value) return - if (mRefreshLoading.value) return - if (mEndOfList.value) return - - listMutex.lock() - - mNextPageLoading.value = true - - @Suppress("TooGenericExceptionCaught") - try { - loadNextPageDeferred = this.async { - val currentList = mStateStorage.value.dataValue() - ?: throw IllegalStateException("Try to load next page when list is empty") - // load next page items - val items = dataSource.loadPage(currentList) - // remove already exist items - val newItems = items.filter { item -> - val existsItem = currentList.firstOrNull { comparator.compare(item, it) == 0 } - existsItem == null - } - // append new items to current list - val newList = currentList.plus(newItems) - // mark end of list if no new items - if (newItems.isEmpty()) { - mEndOfList.value = true - } else { - // save - mStateStorage.value = newList.asState() - } - newList - } - val newList = loadNextPageDeferred!!.await() - - // flag - mNextPageLoading.value = false - // notify - nextPageListener(Result.success(newList)) - } catch (error: Exception) { - // flag - mNextPageLoading.value = false - // notify - nextPageListener(Result.failure(error)) - } - listMutex.unlock() + override fun saveStateNullIsLoading(items: List?) { + _state.value = items.asStateNullIsLoading() } - override suspend fun refreshSuspend() { - loadNextPageDeferred?.cancel() - listMutex.lock() - - if (mRefreshLoading.value) { - listMutex.unlock() - return - } - if (mNextPageLoading.value) { - listMutex.unlock() - return - } - - mRefreshLoading.value = true - - @Suppress("TooGenericExceptionCaught") - try { - // load first page items - val items = dataSource.loadPage(null) - // save - mStateStorage.value = items.asState() - // flag - mEndOfList.value = false - mRefreshLoading.value = false - // notify - refreshListener(Result.success(items)) - } catch (error: Exception) { - // flag - mRefreshLoading.value = false - // notify - refreshListener(Result.failure(error)) - } - listMutex.unlock() + override fun saveErrorState(error: Exception) { + _state.value = error.asState() } - override suspend fun setDataSuspend(items: List?) { - listMutex.lock() - mStateStorage.value = items.asStateNullIsEmpty() - mEndOfList.value = false - listMutex.unlock() + override fun setupLoadingState() { + _state.value = ResourceState.Loading } } -fun List?.asStateNullIsEmpty() = asState { - ResourceState.Empty, E>() +fun List?.asStateNullIsEmpty(): ResourceState, E> = asState { + ResourceState.Empty } -fun List?.asStateNullIsLoading() = asState { - ResourceState.Loading, E>() -} +fun List?.asStateNullIsLoading(): ResourceState, E> = asState { + ResourceState.Loading +} \ No newline at end of file diff --git a/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/PaginationTest.kt b/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/PaginationTest.kt index 7250ee6..a6be748 100644 --- a/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/PaginationTest.kt +++ b/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/PaginationTest.kt @@ -36,7 +36,7 @@ class PaginationTest : BaseTestsClass() { pagination.loadFirstPageSuspend() assertTrue { - pagination.state.value.isSuccess() + pagination.state.value.isData } assertTrue { pagination.state.value.dataValue()!!.compareWith(listOf(0, 1, 2)) diff --git a/paging-livedata/build.gradle.kts b/paging-livedata/build.gradle.kts index 4e22ef0..64c7069 100644 --- a/paging-livedata/build.gradle.kts +++ b/paging-livedata/build.gradle.kts @@ -14,7 +14,6 @@ kotlin { } dependencies { - commonMainImplementation(libs.coroutines) commonMainApi(libs.mokoMvvmLiveData) commonMainApi(libs.mokoMvvmState) commonMainApi(projects.pagingCore) diff --git a/paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/livedata/LiveDataExt.kt b/paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/livedata/LiveDataExt.kt index ac9978d..3260cdc 100644 --- a/paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/livedata/LiveDataExt.kt +++ b/paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/livedata/LiveDataExt.kt @@ -1,5 +1,5 @@ /* - * 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.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 index 794cc7c..2e08c10 100644 --- 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 @@ -1,21 +1,21 @@ /* - * 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.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 -import kotlinx.coroutines.async -import kotlin.coroutines.CoroutineContext class Pagination( - parentScope: CoroutineScope, + private val parentScope: CoroutineScope, dataSource: PagedListDataSource, comparator: Comparator, nextPageListener: (Result>) -> Unit, @@ -29,128 +29,40 @@ class Pagination( refreshListener = refreshListener, ) { - override val coroutineContext: CoroutineContext = parentScope.coroutineContext + private val _state: MutableLiveData, Throwable>> = + MutableLiveData(initValue.asStateNullIsLoading()) - private val mStateStorage = - MutableLiveData, Throwable>>(initValue.asStateNullIsLoading()) + val state: LiveData, Throwable>> + get() = _state.readOnly() - val state = mStateStorage.readOnly() + val refreshLoading: LiveData + get() = mRefreshLoading.asLiveData(parentScope) - private val mNextPageLoading = MutableLiveData(false) - val nextPageLoading = mNextPageLoading.readOnly() + val nextPageLoading: LiveData + get() = mNextPageLoading.asLiveData(parentScope) - private val mEndOfList = MutableLiveData(false) + override fun dataValue(): List? = + _state.value.dataValue() - private val mRefreshLoading = MutableLiveData(false) - val refreshLoading = mRefreshLoading.readOnly() - override suspend fun loadFirstPageSuspend() { - loadNextPageDeferred?.cancel() - - listMutex.lock() - - mEndOfList.value = false - mNextPageLoading.value = false - mStateStorage.value = ResourceState.Loading() + override fun saveState(items: List) { + _state.value = items.asState() + } - @Suppress("TooGenericExceptionCaught") - try { - val items: List = dataSource.loadPage(null) - mStateStorage.value = items.asState() - } catch (error: Exception) { - mStateStorage.value = ResourceState.Failed(error) - } - listMutex.unlock() + override fun saveStateNullIsEmpty(items: List?) { + _state.value = items.asStateNullIsEmpty() } - @Suppress("ReturnCount") - override suspend fun loadNextPageSuspend() { - if (mNextPageLoading.value) return - if (mRefreshLoading.value) return - if (mEndOfList.value) return - - listMutex.lock() - - mNextPageLoading.value = true - - @Suppress("TooGenericExceptionCaught") - try { - loadNextPageDeferred = this.async { - val currentList = mStateStorage.value.dataValue() - ?: throw IllegalStateException("Try to load next page when list is empty") - // load next page items - val items = dataSource.loadPage(currentList) - // remove already exist items - val newItems = items.filter { item -> - val existsItem = currentList.firstOrNull { comparator.compare(item, it) == 0 } - existsItem == null - } - // append new items to current list - val newList = currentList.plus(newItems) - // mark end of list if no new items - if (newItems.isEmpty()) { - mEndOfList.value = true - } else { - // save - mStateStorage.value = newList.asState() - } - newList - } - val newList = loadNextPageDeferred!!.await() - - // flag - mNextPageLoading.value = false - // notify - nextPageListener(Result.success(newList)) - } catch (error: Exception) { - // flag - mNextPageLoading.value = false - // notify - nextPageListener(Result.failure(error)) - } - listMutex.unlock() + override fun saveStateNullIsLoading(items: List?) { + _state.value = items.asStateNullIsLoading() } - override suspend fun refreshSuspend() { - loadNextPageDeferred?.cancel() - listMutex.lock() - - if (mRefreshLoading.value) { - listMutex.unlock() - return - } - if (mNextPageLoading.value) { - listMutex.unlock() - return - } - - mRefreshLoading.value = true - - @Suppress("TooGenericExceptionCaught") - try { - // load first page items - val items = dataSource.loadPage(null) - // save - mStateStorage.value = items.asState() - // flag - mEndOfList.value = false - mRefreshLoading.value = false - // notify - refreshListener(Result.success(items)) - } catch (error: Exception) { - // flag - mRefreshLoading.value = false - // notify - refreshListener(Result.failure(error)) - } - listMutex.unlock() + override fun saveErrorState(error: Exception) { + _state.value = ResourceState.Failed(error) } - override suspend fun setDataSuspend(items: List?) { - listMutex.lock() - mStateStorage.value = items.asStateNullIsEmpty() - mEndOfList.value = false - listMutex.unlock() + override fun setupLoadingState() { + _state.value = ResourceState.Loading() } } diff --git a/paging-state/src/commonMain/kotlin/dev/icerock/moko/paging/state/PagingDataState.kt b/paging-state/src/commonMain/kotlin/dev/icerock/moko/paging/state/PagingDataState.kt deleted file mode 100644 index 67ac4e2..0000000 --- a/paging-state/src/commonMain/kotlin/dev/icerock/moko/paging/state/PagingDataState.kt +++ /dev/null @@ -1,20 +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.state - -sealed interface PagingDataState { - val items: List - data class Normal(override val items: List) : PagingDataState - data class Refresh(override val items: List) : PagingDataState - data class LoadNextPage(override val items: List) : PagingDataState - data class End(override val items: List) : PagingDataState - - val isNormal: Boolean get() = this is Normal<*> - val isRefresh: Boolean get() = this is Refresh<*> - val isLoadingNextPage: Boolean get() = this is LoadNextPage<*> - val isEnd: Boolean get() = this is End<*> - - val isLoading: Boolean get() = !isRefresh -} 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 index b298585..48e4747 100644 --- 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 @@ -1,5 +1,5 @@ /* - * 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.state 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..974157a --- /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 \ No newline at end of file diff --git a/paging-test/build.gradle.kts b/paging-test/build.gradle.kts index d95374e..8d60f21 100644 --- a/paging-test/build.gradle.kts +++ b/paging-test/build.gradle.kts @@ -14,11 +14,12 @@ kotlin { } dependencies { - commonMainApi(libs.coroutines) + androidMainApi(libs.androidCoreTesting) + commonMainApi(projects.pagingCore) commonMainApi(libs.kotlinTestJUnit) - androidMainApi(libs.androidCoreTesting) commonMainApi(libs.ktorClient) commonMainApi(libs.ktorClientMock) + iosX64MainApi(libs.coroutines) } \ No newline at end of file diff --git a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/ListViewModel.kt b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/ListViewModel.kt index f50f251..fc3c966 100644 --- a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/ListViewModel.kt +++ b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/ListViewModel.kt @@ -11,8 +11,9 @@ import dev.icerock.moko.mvvm.livedata.errorTransform import dev.icerock.moko.mvvm.livedata.map import dev.icerock.moko.mvvm.livedata.mediatorOf import dev.icerock.moko.mvvm.viewmodel.ViewModel -import dev.icerock.moko.paging.livedata.IdComparator -import dev.icerock.moko.paging.livedata.IdEntity +import dev.icerock.moko.paging.core.IdComparator +import dev.icerock.moko.paging.core.IdEntity +import dev.icerock.moko.paging.core.LambdaPagedListDataSource import dev.icerock.moko.paging.livedata.Pagination import dev.icerock.moko.units.TableUnitItem import kotlinx.coroutines.delay From 7ebb18cf2afd34a2f7d0b6415d0928c860467836 Mon Sep 17 00:00:00 2001 From: mdubkov Date: Sat, 4 Jun 2022 23:06:19 +0700 Subject: [PATCH 5/6] #39 declarative ui sample & tests --- build.gradle.kts | 1 + gradle/libs.versions.toml | 4 + paging-core/build.gradle.kts | 4 - .../icerock/moko/paging/PagingDeprecated.kt | 40 ++ .../icerock/moko/paging/core/Pagination.kt | 2 +- paging-flow/build.gradle.kts | 4 - .../icerock/moko/paging/flow/Pagination.kt | 15 +- .../moko/paging/flow/IntegrationFlowTests.kt | 38 ++ .../moko/paging/flow/PaginationFlowTest.kt | 53 ++ paging-livedata/build.gradle.kts | 4 - .../moko/paging/PaginationDeprecated.kt | 13 + .../paging/livedata/IntegrationLiveTests.kt | 38 ++ .../moko/paging/livedata/IntegrationTests.kt | 170 ------- .../paging/livedata/PaginationLiveTest.kt | 52 ++ .../moko/paging/livedata/PaginationTest.kt | 137 ----- paging-state/build.gradle.kts | 4 - paging-test/build.gradle.kts | 11 +- .../moko/paging/test}/IntegrationTests.kt | 106 ++-- .../moko/paging/test}/PaginationTest.kt | 76 ++- .../dev/icerock/moko/paging/test/Utils.kt | 4 +- .../moko/paging/test/BaseTestsClass.kt | 7 - .../dev/icerock/moko/paging/test/runTest.kt | 6 - .../androidApp/build.gradle.kts | 61 +++ .../androidApp/src/main/AndroidManifest.xml | 20 + .../declarativeui/android/BindingExt.kt | 32 ++ .../declarativeui/android/ComposeApp.kt | 23 + .../declarativeui/android/ListScreen.kt | 248 +++++++++ .../declarativeui/android/MainActivity.kt | 23 + .../androidApp/src/main/res/values/colors.xml | 6 + .../androidApp/src/main/res/values/styles.xml | 9 + sample-declarative-ui/iosApp/Podfile | 9 + sample-declarative-ui/iosApp/Podfile.lock | 16 + .../iosApp/iosApp.xcodeproj/project.pbxproj | 476 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 ++++ .../iosApp/Assets.xcassets/Contents.json | 6 + .../iosApp/iosApp/ContentView.swift | 21 + .../iosApp/iosApp/Info.plist | 48 ++ .../iosApp/iosApp/ListBinding.swift | 105 ++++ .../iosApp/iosApp/ListScreenBody.swift | 82 +++ .../Preview Assets.xcassets/Contents.json | 6 + .../fromMpp/dev_icerock_moko_mvvm-flow.swift | 173 +++++++ .../dev_icerock_moko_paging-state.swift | 41 ++ .../fromMpp/dev_icerock_moko_resources.swift | 59 +++ .../fromMpp/dev_icerock_moko_shared.swift | 31 ++ .../iosApp/iosApp/iOSApp.swift | 10 + .../iosApp/iosApp/mokoStringDescExt.swift | 42 ++ sample-declarative-ui/shared/build.gradle.kts | 114 +++++ .../src/androidMain/AndroidManifest.xml | 2 + .../paging/sample/declarativeui/ListUnit.kt | 10 + .../sample/declarativeui/ListViewModel.kt | 112 +++++ .../paging/sample/declarativeui/Product.kt | 12 + settings.gradle.kts | 2 + 57 files changed, 2277 insertions(+), 453 deletions(-) create mode 100644 paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/PagingDeprecated.kt create mode 100644 paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/IntegrationFlowTests.kt create mode 100644 paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/PaginationFlowTest.kt create mode 100644 paging-livedata/src/commonMain/kotlin/dev/icerock/moko/paging/PaginationDeprecated.kt create mode 100644 paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/IntegrationLiveTests.kt delete mode 100644 paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/IntegrationTests.kt create mode 100644 paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/PaginationLiveTest.kt delete mode 100644 paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/PaginationTest.kt rename {paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow => paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test}/IntegrationTests.kt (59%) rename {paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow => paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test}/PaginationTest.kt (53%) delete mode 100644 paging-test/src/jvmMain/kotlin/dev/icerock/moko/paging/test/BaseTestsClass.kt delete mode 100644 paging-test/src/jvmMain/kotlin/dev/icerock/moko/paging/test/runTest.kt create mode 100644 sample-declarative-ui/androidApp/build.gradle.kts create mode 100644 sample-declarative-ui/androidApp/src/main/AndroidManifest.xml create mode 100644 sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/paging/sample/declarativeui/android/BindingExt.kt create mode 100644 sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/paging/sample/declarativeui/android/ComposeApp.kt create mode 100644 sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/paging/sample/declarativeui/android/ListScreen.kt create mode 100644 sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/paging/sample/declarativeui/android/MainActivity.kt create mode 100644 sample-declarative-ui/androidApp/src/main/res/values/colors.xml create mode 100644 sample-declarative-ui/androidApp/src/main/res/values/styles.xml create mode 100644 sample-declarative-ui/iosApp/Podfile create mode 100644 sample-declarative-ui/iosApp/Podfile.lock create mode 100644 sample-declarative-ui/iosApp/iosApp.xcodeproj/project.pbxproj create mode 100644 sample-declarative-ui/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 sample-declarative-ui/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 sample-declarative-ui/iosApp/iosApp.xcworkspace/contents.xcworkspacedata create mode 100644 sample-declarative-ui/iosApp/iosApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 sample-declarative-ui/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 sample-declarative-ui/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 sample-declarative-ui/iosApp/iosApp/Assets.xcassets/Contents.json create mode 100644 sample-declarative-ui/iosApp/iosApp/ContentView.swift create mode 100644 sample-declarative-ui/iosApp/iosApp/Info.plist create mode 100644 sample-declarative-ui/iosApp/iosApp/ListBinding.swift create mode 100644 sample-declarative-ui/iosApp/iosApp/ListScreenBody.swift create mode 100644 sample-declarative-ui/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 sample-declarative-ui/iosApp/iosApp/fromMpp/dev_icerock_moko_mvvm-flow.swift create mode 100644 sample-declarative-ui/iosApp/iosApp/fromMpp/dev_icerock_moko_paging-state.swift create mode 100644 sample-declarative-ui/iosApp/iosApp/fromMpp/dev_icerock_moko_resources.swift create mode 100644 sample-declarative-ui/iosApp/iosApp/fromMpp/dev_icerock_moko_shared.swift create mode 100644 sample-declarative-ui/iosApp/iosApp/iOSApp.swift create mode 100644 sample-declarative-ui/iosApp/iosApp/mokoStringDescExt.swift create mode 100644 sample-declarative-ui/shared/build.gradle.kts create mode 100644 sample-declarative-ui/shared/src/androidMain/AndroidManifest.xml create mode 100644 sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/paging/sample/declarativeui/ListUnit.kt create mode 100644 sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/paging/sample/declarativeui/ListViewModel.kt create mode 100644 sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/paging/sample/declarativeui/Product.kt 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 81842c8..292915d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ coroutinesVersion = "1.6.0-native-mt" mokoMvvmVersion = "0.13.0" mokoResourcesVersion = "0.20.1" mokoUnitsVersion = "0.8.0" +mokoKSwiftVersion = "0.5.0" mokoPagingVersion = "0.8.0" [libraries] @@ -26,6 +27,7 @@ mokoMvvmLiveData = { module = "dev.icerock.moko:mvvm-livedata", version.ref = "m 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" } @@ -38,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 index cf1a529..9bc001b 100644 --- a/paging-core/build.gradle.kts +++ b/paging-core/build.gradle.kts @@ -9,10 +9,6 @@ plugins { id("dev.icerock.moko.gradle.detekt") } -kotlin { - jvm() -} - dependencies { commonMainApi(libs.coroutines) } 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/Pagination.kt b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Pagination.kt index bfbc3e0..6d9bbf0 100644 --- a/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Pagination.kt +++ b/paging-core/src/commonMain/kotlin/dev/icerock/moko/paging/core/Pagination.kt @@ -46,7 +46,7 @@ abstract class Pagination( launch { setDataSuspend(items) } } - protected abstract fun dataValue(): List? + abstract fun dataValue(): List? protected abstract fun saveState(items: List) protected abstract fun saveStateNullIsEmpty(items: List?) protected abstract fun saveStateNullIsLoading(items: List?) diff --git a/paging-flow/build.gradle.kts b/paging-flow/build.gradle.kts index 1252c9a..6afed04 100644 --- a/paging-flow/build.gradle.kts +++ b/paging-flow/build.gradle.kts @@ -9,10 +9,6 @@ plugins { id("dev.icerock.moko.gradle.detekt") } -kotlin { - jvm() -} - dependencies { commonMainApi(libs.mokoMvvmFlow) commonMainApi(libs.mokoMvvmCore) 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 index 1b82de6..fb5b17e 100644 --- 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 @@ -4,13 +4,14 @@ 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 -import kotlinx.coroutines.flow.StateFlow typealias PaginationState = ResourceState, Throwable> @@ -32,14 +33,14 @@ class Pagination( private val _state: MutableStateFlow> = MutableStateFlow(initValue.asStateNullIsLoading()) - val state: StateFlow, Throwable>> - get() = _state + val state: CStateFlow, Throwable>> + get() = _state.cStateFlow() - val refreshLoading: StateFlow - get() = mRefreshLoading + val refreshLoading: CStateFlow + get() = mRefreshLoading.cStateFlow() - val nextPageLoading: StateFlow - get() = mNextPageLoading + val nextPageLoading: CStateFlow + get() = mNextPageLoading.cStateFlow() override fun dataValue(): List? = _state.value.dataValue() 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-livedata/build.gradle.kts b/paging-livedata/build.gradle.kts index 64c7069..f04e3b9 100644 --- a/paging-livedata/build.gradle.kts +++ b/paging-livedata/build.gradle.kts @@ -9,10 +9,6 @@ plugins { id("dev.icerock.moko.gradle.detekt") } -kotlin { - jvm() -} - dependencies { commonMainApi(libs.mokoMvvmLiveData) commonMainApi(libs.mokoMvvmState) 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-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/IntegrationTests.kt b/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/IntegrationTests.kt deleted file mode 100644 index c0cfb94..0000000 --- a/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/IntegrationTests.kt +++ /dev/null @@ -1,170 +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.livedata - -import dev.icerock.moko.paging.core.LambdaPagedListDataSource -import dev.icerock.moko.paging.test.BaseTestsClass -import dev.icerock.moko.paging.test.runTest -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.http.fullPath -import kotlinx.coroutines.async -import kotlinx.coroutines.cancel -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlin.test.Test - -class IntegrationTests : BaseTestsClass() { - private val httpClient = HttpClient(MockEngine) { - engine { - addHandler { request -> - when (request.url.fullPath) { - "http://api.icndb.com/jokes/random" -> { - delay(200) - respondOk(""" - { - "type": "success", - "value": { - "id": 318, - "joke": "If you work in an office with Chuck Norris, don't ask him for his three-hole-punch.", - "categories": [] - } - } - """.trimIndent()) - } - else -> error("Unhandled ${request.url.fullPath}") - } - } - } - } - - @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 = { } - ) - - for (i in 0..10) { - println("--- ITERATION $i START ---") - println("start load first page") - pagination.loadFirstPageSuspend() - println("end load first page") - - (0..3).flatMap { - listOf( - async { - println("--> $it refresh start") - pagination.refreshSuspend() - println("<-- $it refresh end") - }, - async { - println("--> $it load next page start") - pagination.loadNextPageSuspend() - println("<-- $it load next page end") - } - ) - }.forEach { it.await() } - } - } - - @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 = { } - ) - - for (i in 0..10) { - println("--- ITERATION $i START ---") - println("start load first page") - pagination.loadFirstPageSuspend() - println("end load first page") - - (0..1).flatMap { - listOf( - async { - println("--> $it refresh start") - pagination.refreshSuspend() - println("<-- $it refresh end") - }, - async { - println("--> $it load next page start") - pagination.loadNextPageSuspend() - println("<-- $it load next page end") - }, - async { - println("--> $it set data start") - val data = pagination.state.value.dataValue().orEmpty() - val newData = data.plus("new item") - pagination.setDataSuspend(newData) - println("--> $it set data end") - } - ) - }.forEach { it.await() } - } - } - - @Test - 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 = { } - ) - - launch { - println("start load") - pagination.loadFirstPageSuspend() - println("end load") - } - - delay(50) - println("cancel scope") - cancel() - } - } - - println(exc) - } -} \ No newline at end of file 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-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/PaginationTest.kt b/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/PaginationTest.kt deleted file mode 100644 index 568caf3..0000000 --- a/paging-livedata/src/commonTest/kotlin/dev/icerock/moko/paging/livedata/PaginationTest.kt +++ /dev/null @@ -1,137 +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.livedata - -import dev.icerock.moko.paging.core.LambdaPagedListDataSource -import dev.icerock.moko.paging.test.BaseTestsClass -import dev.icerock.moko.paging.test.TestListDataSource -import dev.icerock.moko.paging.test.compareWith -import dev.icerock.moko.paging.test.runTest -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.async -import kotlinx.coroutines.delay -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertTrue - -class PaginationTest : BaseTestsClass() { - - var paginationDataSource = TestListDataSource(3, 5) - - val itemsComparator = Comparator { a: Int, b: Int -> - a - b - } - - @BeforeTest - fun setup() { - paginationDataSource = TestListDataSource(3, 5) - } - - @Test - fun `load first page`() = runTest { - val pagination = createPagination() - - pagination.loadFirstPageSuspend() - - assertTrue { - pagination.state.value.isSuccess() - } - assertTrue { - pagination.state.value.dataValue()!!.compareWith(listOf(0, 1, 2)) - } - } - - @Test - fun `load next page`() = runTest { - val pagination = createPagination() - - pagination.loadFirstPageSuspend() - pagination.loadNextPageSuspend() - - assertTrue { - pagination.state.value.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)) - } - } - - @Test - fun `refresh pagination`() = runTest { - val pagination = createPagination() - - pagination.loadFirstPageSuspend() - pagination.loadNextPageSuspend() - pagination.refreshSuspend() - - assertTrue { - pagination.state.value.dataValue()!!.compareWith(listOf(0, 1, 2)) - } - } - - @Test - fun `set data`() = runTest { - val pagination = createPagination() - - pagination.loadFirstPageSuspend() - pagination.loadNextPageSuspend() - - val setList = listOf(5, 2, 3, 1, 4) - pagination.setDataSuspend(setList) - - assertTrue { - pagination.state.value.dataValue()!!.compareWith(setList) - } - } - - @Test - fun `double refresh`() = runTest { - var counter = 0 - val pagination = Pagination( - parentScope = 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") - pagination.loadFirstPageSuspend() - println("end load first page") - - println("start double refresh") - val r1 = async { - pagination.refreshSuspend() - println("first refresh end") - } - val r2 = async { - pagination.refreshSuspend() - println("second refresh end") - } - - 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-state/build.gradle.kts b/paging-state/build.gradle.kts index 22ef75b..3e876b2 100644 --- a/paging-state/build.gradle.kts +++ b/paging-state/build.gradle.kts @@ -8,7 +8,3 @@ plugins { id("dev.icerock.moko.gradle.stub.javadoc") id("dev.icerock.moko.gradle.detekt") } - -kotlin { - jvm() -} diff --git a/paging-test/build.gradle.kts b/paging-test/build.gradle.kts index 8d60f21..0c416f5 100644 --- a/paging-test/build.gradle.kts +++ b/paging-test/build.gradle.kts @@ -9,17 +9,12 @@ plugins { id("dev.icerock.moko.gradle.detekt") } -kotlin { - jvm() -} - dependencies { - androidMainApi(libs.androidCoreTesting) - commonMainApi(projects.pagingCore) commonMainApi(libs.kotlinTestJUnit) commonMainApi(libs.ktorClient) commonMainApi(libs.ktorClientMock) - iosX64MainApi(libs.coroutines) -} \ No newline at end of file + androidMainApi(libs.androidCoreTesting) + iosMainApi(libs.coroutines) +} diff --git a/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/IntegrationTests.kt b/paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test/IntegrationTests.kt similarity index 59% rename from paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/IntegrationTests.kt rename to paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test/IntegrationTests.kt index 4dbbffc..9716365 100644 --- a/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/IntegrationTests.kt +++ b/paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test/IntegrationTests.kt @@ -1,18 +1,20 @@ /* * 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.flow +package dev.icerock.moko.paging.test import dev.icerock.moko.paging.core.LambdaPagedListDataSource -import dev.icerock.moko.paging.test.BaseTestsClass -import dev.icerock.moko.paging.test.runTest +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 @@ -20,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.toString()) { - "http://api.icndb.com/jokes/random" -> { + API_URL -> { delay(200) - respondOk(""" + respondOk( + """ { "type": "success", "value": { @@ -36,7 +63,8 @@ class IntegrationTests : BaseTestsClass() { "categories": [] } } - """.trimIndent()) + """.trimIndent() + ) } else -> error("Unhandled ${request.url.fullPath}") } @@ -45,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 ---") @@ -86,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 = { a, b -> a.compareTo(b) }, - nextPageListener = { }, - refreshListener = { } - ) + open fun parallelRequestsAndSetData() = runTest { + val pagination = createPagination(this) for (i in 0..10) { println("--- ITERATION $i START ---") @@ -123,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") @@ -134,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") @@ -167,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-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/PaginationTest.kt b/paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test/PaginationTest.kt similarity index 53% rename from paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/PaginationTest.kt rename to paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test/PaginationTest.kt index 838a617..e061536 100644 --- a/paging-flow/src/commonTest/kotlin/dev/icerock/moko/paging/flow/PaginationTest.kt +++ b/paging-test/src/commonMain/kotlin/dev/icerock/moko/paging/test/PaginationTest.kt @@ -2,13 +2,11 @@ * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. */ -package dev.icerock.moko.paging.flow +package dev.icerock.moko.paging.test import dev.icerock.moko.paging.core.LambdaPagedListDataSource -import dev.icerock.moko.paging.test.BaseTestsClass -import dev.icerock.moko.paging.test.TestListDataSource -import dev.icerock.moko.paging.test.compareWith -import dev.icerock.moko.paging.test.runTest +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 @@ -16,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.isData + 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() @@ -85,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") @@ -123,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-test/src/iosMain/kotlin/dev/icerock/moko/paging/test/Utils.kt b/paging-test/src/iosMain/kotlin/dev/icerock/moko/paging/test/Utils.kt index 86cdc58..815a842 100644 --- a/paging-test/src/iosMain/kotlin/dev/icerock/moko/paging/test/Utils.kt +++ b/paging-test/src/iosMain/kotlin/dev/icerock/moko/paging/test/Utils.kt @@ -28,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-test/src/jvmMain/kotlin/dev/icerock/moko/paging/test/BaseTestsClass.kt b/paging-test/src/jvmMain/kotlin/dev/icerock/moko/paging/test/BaseTestsClass.kt deleted file mode 100644 index 30d8fe6..0000000 --- a/paging-test/src/jvmMain/kotlin/dev/icerock/moko/paging/test/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.test - -actual open class BaseTestsClass diff --git a/paging-test/src/jvmMain/kotlin/dev/icerock/moko/paging/test/runTest.kt b/paging-test/src/jvmMain/kotlin/dev/icerock/moko/paging/test/runTest.kt deleted file mode 100644 index 5af35c8..0000000 --- a/paging-test/src/jvmMain/kotlin/dev/icerock/moko/paging/test/runTest.kt +++ /dev/null @@ -1,6 +0,0 @@ -package dev.icerock.moko.paging.test - -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) + */ + @discardableResult + public func bindSwitchOn(flow: CStateFlow) -> DisposableHandle { + return UISwitchBindingsKt.bindSwitchOn(self, flow: flow) + } +} + +public extension UIKit.UISwitch { + /** + * selector: PackageFunctionContext/dev.icerock.moko:mvvm-flow/dev.icerock.moko.mvvm.flow.binding/Class(name=platform/UIKit/UISwitch)/bindSwitchOnTwoWay/flow:Class(name=dev/icerock/moko/mvvm/flow/CMutableStateFlow) + */ + @discardableResult + public func bindSwitchOnTwoWay(flow: CMutableStateFlow) -> DisposableHandle { + return UISwitchBindingsKt.bindSwitchOnTwoWay(self, flow: flow) + } +} + +public extension UIKit.UILabel { + /** + * selector: PackageFunctionContext/dev.icerock.moko:mvvm-flow/dev.icerock.moko.mvvm.flow.binding/Class(name=platform/UIKit/UILabel)/bindText/flow:Class(name=dev/icerock/moko/mvvm/flow/CStateFlow) + */ + @discardableResult + public func bindText(flow: CStateFlow) -> DisposableHandle { + return UILabelBindingsKt.bindText(self, flow: flow as! MultiPlatformLibrary.CStateFlow) + } +} + +public extension UIKit.UITextField { + /** + * selector: PackageFunctionContext/dev.icerock.moko:mvvm-flow/dev.icerock.moko.mvvm.flow.binding/Class(name=platform/UIKit/UITextField)/bindText/flow:Class(name=dev/icerock/moko/mvvm/flow/CStateFlow) + */ + @discardableResult + public func bindText(flow: CStateFlow) -> DisposableHandle { + return UITextFieldBindingsKt.bindText(self, flow: flow as! MultiPlatformLibrary.CStateFlow) + } +} + +public extension UIKit.UITextView { + /** + * selector: PackageFunctionContext/dev.icerock.moko:mvvm-flow/dev.icerock.moko.mvvm.flow.binding/Class(name=platform/UIKit/UITextView)/bindText/flow:Class(name=dev/icerock/moko/mvvm/flow/CStateFlow) + */ + @discardableResult + public func bindText(flow: CStateFlow) -> DisposableHandle { + return UITextViewBindingsKt.bindText(self, flow: flow as! MultiPlatformLibrary.CStateFlow) + } +} + +public extension UIKit.UITextField { + /** + * selector: PackageFunctionContext/dev.icerock.moko:mvvm-flow/dev.icerock.moko.mvvm.flow.binding/Class(name=platform/UIKit/UITextField)/bindTextTwoWay/flow:Class(name=dev/icerock/moko/mvvm/flow/CMutableStateFlow) + */ + @discardableResult + public func bindTextTwoWay(flow: CMutableStateFlow) -> DisposableHandle { + return UITextFieldBindingsKt.bindTextTwoWay(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)/bindTextTwoWay/flow:Class(name=dev/icerock/moko/mvvm/flow/CMutableStateFlow) + */ + @discardableResult + public func bindTextTwoWay(flow: CMutableStateFlow) -> DisposableHandle { + return UITextViewBindingsKt.bindTextTwoWay(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)/bindTitle/flow:Class(name=dev/icerock/moko/mvvm/flow/CStateFlow) + */ + @discardableResult + public func bindTitle(flow: CStateFlow) -> DisposableHandle { + return UIButtonBindingsKt.bindTitle(self, flow: flow as! MultiPlatformLibrary.CStateFlow) + } +} diff --git a/sample-declarative-ui/iosApp/iosApp/fromMpp/dev_icerock_moko_paging-state.swift b/sample-declarative-ui/iosApp/iosApp/fromMpp/dev_icerock_moko_paging-state.swift new file mode 100644 index 0000000..07787b6 --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp/fromMpp/dev_icerock_moko_paging-state.swift @@ -0,0 +1,41 @@ +// This file automatically generated by MOKO KSwift (https://github.com/icerockdev/moko-kswift) +// +import MultiPlatformLibrary + +/** + * selector: ClassContext/dev.icerock.moko:paging-state/dev/icerock/moko/paging/state/ResourceState */ +public enum ResourceStateKs { + + case empty + case loading + case data(ResourceStateData) + case error(ResourceStateError) + + var sealed: ResourceState { + switch self { + case .empty: + return MultiPlatformLibrary.ResourceStateEmpty() as! MultiPlatformLibrary.ResourceState + case .loading: + return MultiPlatformLibrary.ResourceStateLoading() as! MultiPlatformLibrary.ResourceState + case .data(let obj): + return obj as! MultiPlatformLibrary.ResourceState + case .error(let obj): + return obj as! MultiPlatformLibrary.ResourceState + } + } + + public init(_ obj: ResourceState) { + if obj is MultiPlatformLibrary.ResourceStateEmpty { + self = .empty + } else if obj is MultiPlatformLibrary.ResourceStateLoading { + self = .loading + } else if let obj = obj as? MultiPlatformLibrary.ResourceStateData { + self = .data(obj) + } else if let obj = obj as? MultiPlatformLibrary.ResourceStateError { + self = .error(obj) + } else { + fatalError("ResourceStateKs not syncronized with ResourceState class") + } + } + +} diff --git a/sample-declarative-ui/iosApp/iosApp/fromMpp/dev_icerock_moko_resources.swift b/sample-declarative-ui/iosApp/iosApp/fromMpp/dev_icerock_moko_resources.swift new file mode 100644 index 0000000..939f6bc --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp/fromMpp/dev_icerock_moko_resources.swift @@ -0,0 +1,59 @@ +// This file automatically generated by MOKO KSwift (https://github.com/icerockdev/moko-kswift) +// +import MultiPlatformLibrary + +/** + * selector: ClassContext/dev.icerock.moko:resources/dev/icerock/moko/resources/ColorResource */ +public enum ColorResourceKs { + + case single(ColorResource.Single) + case themed(ColorResource.Themed) + + var sealed: ColorResource { + switch self { + case .single(let obj): + return obj as! MultiPlatformLibrary.ColorResource + case .themed(let obj): + return obj as! MultiPlatformLibrary.ColorResource + } + } + + public init(_ obj: ColorResource) { + if let obj = obj as? MultiPlatformLibrary.ColorResource.Single { + self = .single(obj) + } else if let obj = obj as? MultiPlatformLibrary.ColorResource.Themed { + self = .themed(obj) + } else { + fatalError("ColorResourceKs not syncronized with ColorResource class") + } + } + +} + +/** + * selector: ClassContext/dev.icerock.moko:resources/dev/icerock/moko/resources/desc/StringDesc.LocaleType */ +public enum StringDescLocaleTypeKs { + + case system + case custom(StringDescLocaleType.Custom) + + var sealed: StringDescLocaleType { + switch self { + case .system: + return MultiPlatformLibrary.StringDescLocaleType.System() as! MultiPlatformLibrary.StringDescLocaleType + case .custom(let obj): + return obj as! MultiPlatformLibrary.StringDescLocaleType + } + } + + public init(_ obj: StringDescLocaleType) { + if obj is MultiPlatformLibrary.StringDescLocaleType.System { + self = .system + } else if let obj = obj as? MultiPlatformLibrary.StringDescLocaleType.Custom { + self = .custom(obj) + } else { + fatalError("StringDescLocaleTypeKs not syncronized with StringDescLocaleType class") + } + } + +} diff --git a/sample-declarative-ui/iosApp/iosApp/fromMpp/dev_icerock_moko_shared.swift b/sample-declarative-ui/iosApp/iosApp/fromMpp/dev_icerock_moko_shared.swift new file mode 100644 index 0000000..c9d09a0 --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp/fromMpp/dev_icerock_moko_shared.swift @@ -0,0 +1,31 @@ +// This file automatically generated by MOKO KSwift (https://github.com/icerockdev/moko-kswift) +// +import MultiPlatformLibrary + +/** + * selector: ClassContext/dev.icerock.moko:shared/dev/icerock/moko/paging/sample/declarativeui/ListUnit */ +public enum ListUnitKs { + + case productUnit(ListUnitProductUnit) + case loading + + var sealed: ListUnit { + switch self { + case .productUnit(let obj): + return obj as! MultiPlatformLibrary.ListUnit + case .loading: + return MultiPlatformLibrary.ListUnitLoading() as! MultiPlatformLibrary.ListUnit + } + } + + public init(_ obj: ListUnit) { + if let obj = obj as? MultiPlatformLibrary.ListUnitProductUnit { + self = .productUnit(obj) + } else if obj is MultiPlatformLibrary.ListUnitLoading { + self = .loading + } else { + fatalError("ListUnitKs not syncronized with ListUnit class") + } + } + +} diff --git a/sample-declarative-ui/iosApp/iosApp/iOSApp.swift b/sample-declarative-ui/iosApp/iosApp/iOSApp.swift new file mode 100644 index 0000000..0648e86 --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp/iOSApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct iOSApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} \ No newline at end of file diff --git a/sample-declarative-ui/iosApp/iosApp/mokoStringDescExt.swift b/sample-declarative-ui/iosApp/iosApp/mokoStringDescExt.swift new file mode 100644 index 0000000..edaa566 --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp/mokoStringDescExt.swift @@ -0,0 +1,42 @@ +// +// mokoStringDescExt.swift +// iosApp +// +// Created by mdubkov on 02.06.2022. +// Copyright © 2022 orgName. All rights reserved. +// + +import Foundation +import MultiPlatformLibrary +import mokoMvvmFlowSwiftUI + +extension ObservableObject where Self: ViewModel { + func stateNullable( + _ flowKey: KeyPath>, + equals: @escaping (T?, T?) -> Bool, + mapper: @escaping (T?) -> R? + ) -> R? { + let stateFlow: CStateFlow = self[keyPath: flowKey] + var lastValue: T? = stateFlow.value + + var disposable: DisposableHandle? = nil + + disposable = stateFlow.subscribe(onCollect: { value in + if !equals(lastValue, value) { + lastValue = value + self.objectWillChange.send() + disposable?.dispose() + } + }) + + return mapper(stateFlow.value) + } + + func stateNullable(_ flowKey: KeyPath>) -> String? { + return stateNullable( + flowKey, + equals: { $0 === $1 }, + mapper: { $0?.localized() } + ) + } +} diff --git a/sample-declarative-ui/shared/build.gradle.kts b/sample-declarative-ui/shared/build.gradle.kts new file mode 100644 index 0000000..cbfdd92 --- /dev/null +++ b/sample-declarative-ui/shared/build.gradle.kts @@ -0,0 +1,114 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework + +plugins { + kotlin("multiplatform") + id("com.android.library") + id("dev.icerock.moko.kswift") +} + +val dependenciesList = listOf( + libs.mokoMvvmFlow, + libs.mokoMvvmCore, + projects.pagingFlow, + projects.pagingState, + libs.mokoResources +) + +kswift { + install(dev.icerock.moko.kswift.plugin.feature.SealedToSwiftEnumFeature) +} + +kotlin { + android() + + val xcf = XCFramework("MultiPlatformLibrary") + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { target -> + target.binaries.framework { + baseName = "MultiPlatformLibrary" + + xcf.add(this) + dependenciesList.forEach { export(it) } + } + } + + sourceSets { + val commonMain by getting { + dependencies { + api(libs.coroutines) + dependenciesList.forEach { api(it) } + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + val androidMain by getting { + dependencies { + api(libs.mokoMvvmFlowCompose) + } + } + val androidTest by getting + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + val iosX64Test by getting + val iosArm64Test by getting + val iosSimulatorArm64Test by getting + val iosTest by creating { + dependsOn(commonTest) + iosX64Test.dependsOn(this) + iosArm64Test.dependsOn(this) + iosSimulatorArm64Test.dependsOn(this) + } + } +} + +android { + compileSdk = 32 + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + defaultConfig { + minSdk = 21 + targetSdk = 32 + } +} + +afterEvaluate { + tasks.filterIsInstance() + .forEach { xcFrameworkTask -> + val syncName: String = xcFrameworkTask.name.replace("assemble", "sync") + + tasks.create(syncName, Sync::class) { + this.group = "xcode" + + this.from(File(xcFrameworkTask.outputDir, xcFrameworkTask.buildType.getName())) + this.into(File(project.buildDir, "xcode")) + + this.dependsOn(xcFrameworkTask) + } + } +} + +kotlin.targets.withType().configureEach { + binaries.withType().configureEach { + linkTask.doLast { + val from = File(outputDirectory, "${baseName}Swift") + val to = File(rootDir, "sample-declarative-ui/iosApp/iosApp/fromMpp") + from.copyRecursively(to, overwrite = true) + } + } +} diff --git a/sample-declarative-ui/shared/src/androidMain/AndroidManifest.xml b/sample-declarative-ui/shared/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..b7f1d46 --- /dev/null +++ b/sample-declarative-ui/shared/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/paging/sample/declarativeui/ListUnit.kt b/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/paging/sample/declarativeui/ListUnit.kt new file mode 100644 index 0000000..015580b --- /dev/null +++ b/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/paging/sample/declarativeui/ListUnit.kt @@ -0,0 +1,10 @@ +/* + * 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 + +sealed interface ListUnit { + data class ProductUnit(val id: Long, val title: String) : ListUnit + object Loading : ListUnit +} \ No newline at end of file diff --git a/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/paging/sample/declarativeui/ListViewModel.kt b/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/paging/sample/declarativeui/ListViewModel.kt new file mode 100644 index 0000000..bb5c389 --- /dev/null +++ b/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/paging/sample/declarativeui/ListViewModel.kt @@ -0,0 +1,112 @@ +/* + * 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 + +import dev.icerock.moko.mvvm.flow.CStateFlow +import dev.icerock.moko.mvvm.flow.cStateFlow +import dev.icerock.moko.mvvm.viewmodel.ViewModel +import dev.icerock.moko.paging.core.IdComparator +import dev.icerock.moko.paging.core.LambdaPagedListDataSource +import dev.icerock.moko.paging.flow.Pagination +import dev.icerock.moko.paging.state.ResourceState +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext + +private const val PAGE_LOAD_DURATION_MS: Long = 2000 + +class ListViewModel( + withInitialValue: Boolean = false, + withError: Boolean = false +) : ViewModel() { + private val pagination: Pagination = Pagination( + parentScope = viewModelScope, + dataSource = LambdaPagedListDataSource { + delay(PAGE_LOAD_DURATION_MS) + if (withError) throw IllegalStateException() + it?.plus(generatePack(it.size.toLong())) ?: generatePack() + }, + comparator = IdComparator(), + nextPageListener = ::onNextPageResult, + refreshListener = ::onRefreshResult, + initValue = if (withInitialValue) generatePack() else null + ) + + fun start() { + pagination.loadFirstPage() + } + + val isRefreshing: CStateFlow get() = pagination.refreshLoading + + val state: CStateFlow, String>> = combine( + pagination.state, + pagination.nextPageLoading + ) { state, nextPageLoading -> + return@combine when (state) { + is ResourceState.Data -> { + if (state.data.isEmpty()) return@combine ResourceState.Empty + + val data: List = buildList { + state.data.map { + ListUnit.ProductUnit(id = it.id, title = it.title) + }.let { addAll(it) } + if (nextPageLoading) add(ListUnit.Loading) + } + + ResourceState.Data(data) + } + is ResourceState.Error -> ResourceState.Error(state.error.toString()) + ResourceState.Empty -> ResourceState.Empty + ResourceState.Loading -> ResourceState.Loading + } + }.stateIn(viewModelScope, SharingStarted.Eagerly, ResourceState.Loading).cStateFlow() + + fun onRetryPressed() { + pagination.loadFirstPage() + } + + fun onLoadNextPage() { + pagination.loadNextPage() + } + + fun onRefresh() { + pagination.refresh() + } + + suspend fun onRefreshSuspend() { + withContext(viewModelScope.coroutineContext) { + pagination.refreshSuspend() + } + } + + private fun onNextPageResult(result: Result>) { + if (result.isSuccess) { + println("next page successful loaded") + } else { + println("next page loading failed ${result.exceptionOrNull()}") + } + } + + private fun onRefreshResult(result: Result>) { + if (result.isSuccess) { + println("refresh successful") + } else { + println("refresh failed ${result.exceptionOrNull()}") + } + } + + @Suppress("MagicNumber") + private fun generatePack(startId: Long = 0): List { + return List(20) { idx -> + val id = startId + idx + Product( + id = id, + title = "Product $id" + ) + } + } +} diff --git a/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/paging/sample/declarativeui/Product.kt b/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/paging/sample/declarativeui/Product.kt new file mode 100644 index 0000000..1752fb1 --- /dev/null +++ b/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/paging/sample/declarativeui/Product.kt @@ -0,0 +1,12 @@ +/* + * 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 + +import dev.icerock.moko.paging.core.IdEntity + +data class Product( + override val id: Long, + val title: String +) : IdEntity \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index d6a75ec..9b25c66 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,3 +31,5 @@ include(":paging-state") include(":paging-test") include(":sample:android-app") include(":sample:mpp-library") +include(":sample-declarative-ui:androidApp") +include(":sample-declarative-ui:shared") From 0f4105be450ad8b0c95d6216ad6ff21441ebcd6c Mon Sep 17 00:00:00 2001 From: mdubkov Date: Sat, 4 Jun 2022 23:21:42 +0700 Subject: [PATCH 6/6] #39 update README --- README.md | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) 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