From 2f3ac0fead325a53a97c2f2b1fd1c258ddcf2d1e Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sat, 20 Jan 2024 14:53:06 +0700 Subject: [PATCH 01/28] add lifecycle module --- gradle/libs.versions.toml | 1 + khonshu-navigation-core/build.gradle.kts | 2 + .../lifecycleAndroidSupport.android.kt | 18 -- .../navigation/LocalLifecycleOwner.kt | 12 ++ .../solivagant/navigation/NavigationSetup.kt | 10 +- .../internal/lifecycleAndroidSupport.kt | 12 -- .../internal/lifecycleAndroidSupport.ios.kt | 51 +++++ lifecycle/build.gradle.kts | 202 ++++++++++++++++++ .../hoc081098/solivagant/lifecycle/android.kt | 78 +++++++ .../solivagant/lifecycle/internal/mapState.kt | 27 +++ .../solivagant/lifecycle/Lifecycle.kt | 133 ++++++++++++ .../solivagant/lifecycle/LifecycleOwner.kt | 15 ++ .../lifecycle/LifecycleOwnerRegistry.kt | 113 ++++++++++ .../solivagant/lifecycle/repeatOnLifecycle.kt | 111 ++++++++++ .../rememberPlatformLifecycleOwner.android.kt | 13 ++ .../solivagant/navigation/NavHost.kt | 9 +- .../rememberPlatformLifecycleOwner.kt | 7 + settings.gradle.kts | 1 + 18 files changed, 779 insertions(+), 36 deletions(-) delete mode 100644 khonshu-navigation-core/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/lifecycleAndroidSupport.android.kt create mode 100644 khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt delete mode 100644 khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/lifecycleAndroidSupport.kt create mode 100644 khonshu-navigation-core/src/iosMain/kotlin/com/hoc081098/solivagant/navigation/internal/lifecycleAndroidSupport.ios.kt create mode 100644 lifecycle/build.gradle.kts create mode 100644 lifecycle/src/androidMain/kotlin/com/hoc081098/solivagant/lifecycle/android.kt create mode 100644 lifecycle/src/androidMain/kotlin/com/hoc081098/solivagant/lifecycle/internal/mapState.kt create mode 100644 lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/Lifecycle.kt create mode 100644 lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LifecycleOwner.kt create mode 100644 lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LifecycleOwnerRegistry.kt create mode 100644 lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/repeatOnLifecycle.kt create mode 100644 navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.android.kt create mode 100644 navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fc5e87bf..0c4eedc6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -79,6 +79,7 @@ koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", versi coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } compose-rules-detekt = { module = "io.nlopez.compose.rules:detekt", version.ref = "compose-rules-detekt" } +androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "androidx-lifecycle" } androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version = "androidx-lifecycle" } diff --git a/khonshu-navigation-core/build.gradle.kts b/khonshu-navigation-core/build.gradle.kts index e690f43c..b0e5cd16 100644 --- a/khonshu-navigation-core/build.gradle.kts +++ b/khonshu-navigation-core/build.gradle.kts @@ -73,7 +73,9 @@ kotlin { commonMain { dependencies { api(compose.runtime) + api(compose.ui) + api(projects.lifecycle) api(libs.kmp.viewmodel.core) api(libs.kmp.viewmodel.savedstate) api(libs.kmp.viewmodel.compose) diff --git a/khonshu-navigation-core/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/lifecycleAndroidSupport.android.kt b/khonshu-navigation-core/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/lifecycleAndroidSupport.android.kt deleted file mode 100644 index f5c78e05..00000000 --- a/khonshu-navigation-core/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/lifecycleAndroidSupport.android.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.hoc081098.solivagant.navigation.internal - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle - -// TODO: https://youtrack.jetbrains.com/issue/KT-37316 -@Suppress("ACTUAL_WITHOUT_EXPECT") // internal expect is not matched with internal typealias to public type -internal actual typealias LifecycleOwner = androidx.lifecycle.LifecycleOwner - -@Composable -@ReadOnlyComposable -internal actual fun currentLifecycleOwner(): LifecycleOwner = LocalLifecycleOwner.current - -internal actual suspend fun LifecycleOwner.repeatOnResumeLifecycle(block: suspend () -> Unit) = - repeatOnLifecycle(state = Lifecycle.State.RESUMED) { block() } diff --git a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt new file mode 100644 index 00000000..f6637097 --- /dev/null +++ b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt @@ -0,0 +1,12 @@ +package com.hoc081098.solivagant.navigation + +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.staticCompositionLocalOf +import com.hoc081098.solivagant.lifecycle.LifecycleOwner + +/** + * The CompositionLocal containing the current [LifecycleOwner]. + */ +public val LocalLifecycleOwner: ProvidableCompositionLocal = staticCompositionLocalOf { + error("CompositionLocal LocalLifecycleOwner not present") +} diff --git a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavigationSetup.kt b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavigationSetup.kt index 1ed8d6e3..d449c267 100644 --- a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavigationSetup.kt +++ b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavigationSetup.kt @@ -7,14 +7,14 @@ import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.staticCompositionLocalOf import com.hoc081098.kmp.viewmodel.parcelable.Parcelable import com.hoc081098.kmp.viewmodel.parcelable.Parcelize +import com.hoc081098.solivagant.lifecycle.Lifecycle +import com.hoc081098.solivagant.lifecycle.LifecycleOwner +import com.hoc081098.solivagant.lifecycle.repeatOnLifecycle import com.hoc081098.solivagant.navigation.internal.InternalNavigationApi -import com.hoc081098.solivagant.navigation.internal.LifecycleOwner import com.hoc081098.solivagant.navigation.internal.NavEvent import com.hoc081098.solivagant.navigation.internal.NavigationExecutor import com.hoc081098.solivagant.navigation.internal.VisibleForTesting import com.hoc081098.solivagant.navigation.internal.currentBackPressedDispatcher -import com.hoc081098.solivagant.navigation.internal.currentLifecycleOwner -import com.hoc081098.solivagant.navigation.internal.repeatOnResumeLifecycle import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -41,7 +41,7 @@ public fun NavigationSetup(navigator: NavEventNavigator) { } } - val lifecycleOwner = currentLifecycleOwner() + val lifecycleOwner = LocalLifecycleOwner.current LaunchedEffect(lifecycleOwner, executor, navigator) { navigator.collectAndHandleNavEvents(lifecycleOwner, executor) } @@ -63,7 +63,7 @@ internal suspend fun NavEventNavigator.collectAndHandleNavEvents( // whichever comes first. Basically, it has some certain delay compared to [Dispatchers.Main.immediate]. // So we must switch to [Dispatchers.Main.immediate] before collecting events. withContext(Dispatchers.Main.immediate) { - lifecycleOwner.repeatOnResumeLifecycle { + lifecycleOwner.repeatOnLifecycle(state = Lifecycle.State.RESUMED) { navEvents.collect { event -> executor.navigateTo(event) } diff --git a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/lifecycleAndroidSupport.kt b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/lifecycleAndroidSupport.kt deleted file mode 100644 index 084d7978..00000000 --- a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/lifecycleAndroidSupport.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.hoc081098.solivagant.navigation.internal - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable - -internal expect interface LifecycleOwner - -@Composable -@ReadOnlyComposable -internal expect fun currentLifecycleOwner(): LifecycleOwner - -internal expect suspend fun LifecycleOwner.repeatOnResumeLifecycle(block: suspend () -> Unit) diff --git a/khonshu-navigation-core/src/iosMain/kotlin/com/hoc081098/solivagant/navigation/internal/lifecycleAndroidSupport.ios.kt b/khonshu-navigation-core/src/iosMain/kotlin/com/hoc081098/solivagant/navigation/internal/lifecycleAndroidSupport.ios.kt new file mode 100644 index 00000000..fadfb9ea --- /dev/null +++ b/khonshu-navigation-core/src/iosMain/kotlin/com/hoc081098/solivagant/navigation/internal/lifecycleAndroidSupport.ios.kt @@ -0,0 +1,51 @@ +package com.hoc081098.solivagant.navigation.internal + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.interop.LocalUIViewController +import platform.Foundation.NSNotification +import platform.Foundation.NSNotificationCenter +import platform.Foundation.NSNotificationName +import platform.Foundation.NSOperationQueue +import platform.UIKit.UIApplication +import platform.UIKit.UIApplicationDidBecomeActiveNotification +import platform.UIKit.UIApplicationDidEnterBackgroundNotification +import platform.UIKit.UIApplicationWillEnterForegroundNotification +import platform.UIKit.UIApplicationWillResignActiveNotification +import platform.UIKit.UIApplicationWillTerminateNotification +import platform.UIKit.UIViewController +import platform.darwin.NSObjectProtocol + +internal actual interface LifecycleOwner + +private class LifecycleOwnerImpl( + private val uiViewController: UIViewController, +) : LifecycleOwner { + + private val willEnterForegroundObserver = addObserver(UIApplicationWillEnterForegroundNotification) { lifecycle.start() } + private val didBecomeActiveObserver = addObserver(UIApplicationDidBecomeActiveNotification) { lifecycle.resume() } + private val willResignActiveObserver = addObserver(UIApplicationWillResignActiveNotification) { lifecycle.pause() } + private val didEnterBackgroundObserver = addObserver(UIApplicationDidEnterBackgroundNotification) { lifecycle.stop() } + private val willTerminateObserver = addObserver(UIApplicationWillTerminateNotification) { lifecycle.destroy() } + + init { + } +} + +@Composable +@ReadOnlyComposable +internal actual fun currentLifecycleOwner(): LifecycleOwner = LifecycleOwnerImpl( + uiViewController = LocalUIViewController.current, +) + +internal actual suspend fun LifecycleOwner.repeatOnResumeLifecycle(block: suspend () -> Unit) { + UIApplication.sharedApplication().delegate +} + +private fun addObserver(name: NSNotificationName, block: (NSNotification?) -> Unit): NSObjectProtocol = + NSNotificationCenter.defaultCenter.addObserverForName( + name = name, + `object` = null, + queue = NSOperationQueue.mainQueue, + usingBlock = block, + ) diff --git a/lifecycle/build.gradle.kts b/lifecycle/build.gradle.kts new file mode 100644 index 00000000..61104cb4 --- /dev/null +++ b/lifecycle/build.gradle.kts @@ -0,0 +1,202 @@ +@file:Suppress("ClassName") + +import java.net.URL +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.poko) + + alias(libs.plugins.vanniktech.maven.publish) + alias(libs.plugins.dokka) + alias(libs.plugins.kotlinx.binary.compatibility.validator) + alias(libs.plugins.kotlinx.kover) +} + +kotlin { + explicitApi() + + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(libs.versions.java.toolchain.get())) + vendor.set(JvmVendorSpec.AZUL) + } + + androidTarget { + publishAllLibraryVariants() + + compilations.configureEach { + compilerOptions.configure { + jvmTarget.set(JvmTarget.fromTarget(libs.versions.java.target.get())) + } + } + } + + jvm { + compilations.configureEach { + compilerOptions.configure { + jvmTarget.set(JvmTarget.fromTarget(libs.versions.java.target.get())) + } + } + } + + js(IR) { + compilations.all { + kotlinOptions { + sourceMap = true + moduleKind = "commonjs" + } + } + browser() + nodejs() + } + + iosArm64() + iosX64() + iosSimulatorArm64() + + macosX64() + macosArm64() + + applyDefaultHierarchyTemplate() + + sourceSets { + commonMain { + dependencies { + api(libs.coroutines.core) + } + } + commonTest { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + + val commonJvmMain by creating { + dependsOn(commonMain.get()) + } + val commonJvmTest by creating { + dependsOn(commonTest.get()) + } + + val nonJvmMain by creating { + dependsOn(commonMain.get()) + } + val nonJvmTest by creating { + dependsOn(commonTest.get()) + } + + androidMain { + dependsOn(commonJvmMain) + + dependencies { + implementation(libs.androidx.lifecycle.runtime.ktx) + } + } + val androidUnitTest by getting { + dependsOn(commonJvmTest) + + dependencies { + implementation(kotlin("test-junit")) + } + } + + val nonAndroidMain by creating { + dependsOn(commonMain.get()) + } + val nonAndroidTest by creating { + dependsOn(commonTest.get()) + } + + jvmMain { + dependsOn(nonAndroidMain) + dependsOn(commonJvmMain) + } + jvmTest { + dependsOn(nonAndroidTest) + dependsOn(commonJvmTest) + + dependencies { + implementation(kotlin("test-junit")) + } + } + + jsMain { + dependsOn(nonAndroidMain) + dependsOn(nonJvmMain) + } + jsTest { + dependsOn(nonAndroidTest) + dependsOn(nonJvmTest) + + dependencies { + implementation(kotlin("test-js")) + } + } + + nativeMain { + dependsOn(nonAndroidMain) + dependsOn(nonJvmMain) + } + nativeTest { + dependsOn(nonAndroidTest) + dependsOn(nonJvmTest) + } + } + + sourceSets.matching { it.name.contains("Test") }.all { + languageSettings { + optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") + } + } +} + +tasks.withType>().configureEach { + kotlinOptions { + // 'expect'/'actual' classes (including interfaces, objects, annotations, enums, + // and 'actual' typealiases) are in Beta. + // You can use -Xexpect-actual-classes flag to suppress this warning. + // Also see: https://youtrack.jetbrains.com/issue/KT-61573 + freeCompilerArgs += + listOf( + "-Xexpect-actual-classes", + ) + } +} + +android { + compileSdk = libs.versions.android.compile.get().toInt() + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + namespace = "com.hoc081098.solivagant.lifecycle" + + defaultConfig { + minSdk = libs.versions.android.min.get().toInt() + } + + // still needed for Android projects despite toolchain + compileOptions { + sourceCompatibility = JavaVersion.toVersion(libs.versions.java.target.get()) + targetCompatibility = JavaVersion.toVersion(libs.versions.java.target.get()) + } +} + +mavenPublishing { + publishToMavenCentral(com.vanniktech.maven.publish.SonatypeHost.S01, automaticRelease = true) + signAllPublications() +} + +tasks.withType().configureEach { + dokkaSourceSets { + configureEach { + externalDocumentationLink("https://kotlinlang.org/api/kotlinx.coroutines/") + + sourceLink { + localDirectory.set(projectDir.resolve("src")) + remoteUrl.set(URL("https://github.com/hoc081098/solivagant/tree/master/lifecycle/src")) + remoteLineSuffix.set("#L") + } + } + } +} diff --git a/lifecycle/src/androidMain/kotlin/com/hoc081098/solivagant/lifecycle/android.kt b/lifecycle/src/androidMain/kotlin/com/hoc081098/solivagant/lifecycle/android.kt new file mode 100644 index 00000000..8ec72012 --- /dev/null +++ b/lifecycle/src/androidMain/kotlin/com/hoc081098/solivagant/lifecycle/android.kt @@ -0,0 +1,78 @@ +package com.hoc081098.solivagant.lifecycle + +import androidx.lifecycle.Lifecycle as AndroidXLifecycle +import androidx.lifecycle.Lifecycle.Event as AndroidXLifecycleEvent +import androidx.lifecycle.Lifecycle.State as AndroidXLifecycleState +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner as AndroidXLifecycleOwner +import com.hoc081098.solivagant.lifecycle.Lifecycle.Cancellable +import com.hoc081098.solivagant.lifecycle.Lifecycle.Event +import com.hoc081098.solivagant.lifecycle.Lifecycle.State +import com.hoc081098.solivagant.lifecycle.internal.mapState +import kotlin.LazyThreadSafetyMode.NONE +import kotlinx.coroutines.flow.StateFlow + +public fun AndroidXLifecycle.asSolivagantLifecycle(): Lifecycle = + SolivagantLifecycleInterop(this) + +public fun AndroidXLifecycleOwner.asSolivagantLifecycleOwner(): LifecycleOwner = + SolivagantLifecycleOwnerInterop(this) + +public fun AndroidXLifecycleState.asSolivagantState(): State = when (this) { + AndroidXLifecycleState.DESTROYED -> State.DESTROYED + AndroidXLifecycleState.INITIALIZED -> State.INITIALIZED + AndroidXLifecycleState.CREATED -> State.CREATED + AndroidXLifecycleState.STARTED -> State.STARTED + AndroidXLifecycleState.RESUMED -> State.RESUMED +} + +public fun AndroidXLifecycleEvent.asSolivagantEventOrNull(): Event? = when (this) { + AndroidXLifecycleEvent.ON_CREATE -> Event.ON_CREATE + AndroidXLifecycleEvent.ON_START -> Event.ON_START + AndroidXLifecycleEvent.ON_RESUME -> Event.ON_RESUME + AndroidXLifecycleEvent.ON_PAUSE -> Event.ON_PAUSE + AndroidXLifecycleEvent.ON_STOP -> Event.ON_STOP + AndroidXLifecycleEvent.ON_DESTROY -> Event.ON_DESTROY + AndroidXLifecycleEvent.ON_ANY -> null +} + +private class SolivagantLifecycleInterop( + private val delegate: AndroidXLifecycle, +) : Lifecycle { + override val currentStateFlow: StateFlow by lazy(NONE) { + delegate.currentStateFlow.mapState { it.asSolivagantState() } + } + + override fun subscribe(observer: Lifecycle.Observer): Cancellable { + // @MainThread + var removedObserver = false + + val lifecycleEventObserver = object : LifecycleEventObserver { + override fun onStateChanged(source: AndroidXLifecycleOwner, event: AndroidXLifecycleEvent) { + event + .asSolivagantEventOrNull() + ?.let(observer::onStateChanged) + + // remove observer when ON_DESTROY + if (event == AndroidXLifecycleEvent.ON_DESTROY) { + delegate.removeObserver(this) + removedObserver = true + } + } + }.also(delegate::addObserver) + + return Cancellable { + if (!removedObserver) { + delegate.removeObserver(lifecycleEventObserver) + } + } + } +} + +private class SolivagantLifecycleOwnerInterop( + private val delegate: AndroidXLifecycleOwner, +) : LifecycleOwner { + override val lifecycle: Lifecycle by lazy(NONE) { + delegate.lifecycle.asSolivagantLifecycle() + } +} diff --git a/lifecycle/src/androidMain/kotlin/com/hoc081098/solivagant/lifecycle/internal/mapState.kt b/lifecycle/src/androidMain/kotlin/com/hoc081098/solivagant/lifecycle/internal/mapState.kt new file mode 100644 index 00000000..530599ad --- /dev/null +++ b/lifecycle/src/androidMain/kotlin/com/hoc081098/solivagant/lifecycle/internal/mapState.kt @@ -0,0 +1,27 @@ +package com.hoc081098.solivagant.lifecycle.internal + +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +/** + * Map a [StateFlow] to another [StateFlow] with the given [transform] function. + */ +internal fun StateFlow.mapState(transform: (T) -> R): StateFlow = + object : StateFlow { + override val replayCache: List + get() = this@mapState.replayCache.map(transform) + + override val value: R + get() = transform(this@mapState.value) + + override suspend fun collect(collector: FlowCollector): Nothing { + this@mapState + .map { transform(it) } + .distinctUntilChanged() + .collect(collector) + + error("StateFlow collection never ends.") + } + } diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/Lifecycle.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/Lifecycle.kt new file mode 100644 index 00000000..aec152b5 --- /dev/null +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/Lifecycle.kt @@ -0,0 +1,133 @@ +package com.hoc081098.solivagant.lifecycle + +import kotlin.jvm.JvmStatic +import kotlinx.coroutines.flow.StateFlow + +/** + * A holder of [Lifecycle.State] that can be observed for changes. + * + * Possible transitions: + * + * ``` + * [INITIALIZED] ──┐ + * ↓ + * ┌── [CREATED] ──┐ + * ↓ ↑ ↓ + * [DESTROYED] └── [STARTED] ──┐ + * ↑ ↓ + * └── [RESUMED] + * ``` + */ +public interface Lifecycle { + public val currentStateFlow: StateFlow + + public fun subscribe(observer: Observer): Cancellable + + public fun interface Observer { + public fun onStateChanged(event: Event) + } + + public fun interface Cancellable { + public fun cancel() + } + + /** + * Defines the possible states of the [Lifecycle]. + */ + public enum class State { + DESTROYED, + INITIALIZED, + CREATED, + STARTED, + RESUMED, + } + + public enum class Event { + ON_CREATE, + ON_START, + ON_RESUME, + ON_PAUSE, + ON_STOP, + ON_DESTROY, + ; + + public companion object { + /** + * Returns the [Lifecycle.Event] that will be reported by a [Lifecycle] + * leaving the specified [Lifecycle.State] to a lower state, or `null` + * if there is no valid event that can move down from the given state. + * + * @param state the higher state that the returned event will transition down from + * @return the event moving down the lifecycle phases from state + */ + @JvmStatic + public fun downFrom(state: State): Event? { + return when (state) { + State.CREATED -> ON_DESTROY + State.STARTED -> ON_STOP + State.RESUMED -> ON_PAUSE + else -> null + } + } + + /** + * Returns the [Lifecycle.Event] that will be reported by a [Lifecycle] + * entering the specified [Lifecycle.State] from a higher state, or `null` + * if there is no valid event that can move down to the given state. + * + * @param state the lower state that the returned event will transition down to + * @return the event moving down the lifecycle phases to state + */ + @JvmStatic + public fun downTo(state: State): Event? { + return when (state) { + State.DESTROYED -> ON_DESTROY + State.CREATED -> ON_STOP + State.STARTED -> ON_PAUSE + else -> null + } + } + + /** + * Returns the [Lifecycle.Event] that will be reported by a [Lifecycle] + * leaving the specified [Lifecycle.State] to a higher state, or `null` + * if there is no valid event that can move up from the given state. + * + * @param state the lower state that the returned event will transition up from + * @return the event moving up the lifecycle phases from state + */ + @JvmStatic + public fun upFrom(state: State): Event? { + return when (state) { + State.INITIALIZED -> ON_CREATE + State.CREATED -> ON_START + State.STARTED -> ON_RESUME + else -> null + } + } + + /** + * Returns the [Lifecycle.Event] that will be reported by a [Lifecycle] + * entering the specified [Lifecycle.State] from a lower state, or `null` + * if there is no valid event that can move up to the given state. + * + * @param state the higher state that the returned event will transition up to + * @return the event moving up the lifecycle phases to state + */ + @JvmStatic + public fun upTo(state: State): Event? { + return when (state) { + State.CREATED -> ON_CREATE + State.STARTED -> ON_START + State.RESUMED -> ON_RESUME + else -> null + } + } + } + } + + public companion object { + public val Lifecycle.currentState: State + get() = currentStateFlow.value + } +} diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LifecycleOwner.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LifecycleOwner.kt new file mode 100644 index 00000000..dc7646b2 --- /dev/null +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LifecycleOwner.kt @@ -0,0 +1,15 @@ +package com.hoc081098.solivagant.lifecycle + +/** + * A class that has an lifecycle. + * + * @see Lifecycle + */ +public interface LifecycleOwner { + /** + * Returns the Lifecycle of the provider. + * + * @return The lifecycle of the provider. + */ + public val lifecycle: Lifecycle +} diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LifecycleOwnerRegistry.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LifecycleOwnerRegistry.kt new file mode 100644 index 00000000..3169bfe2 --- /dev/null +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LifecycleOwnerRegistry.kt @@ -0,0 +1,113 @@ +package com.hoc081098.solivagant.lifecycle + +import com.hoc081098.solivagant.lifecycle.Lifecycle.Cancellable +import com.hoc081098.solivagant.lifecycle.Lifecycle.Event +import com.hoc081098.solivagant.lifecycle.Lifecycle.Observer +import com.hoc081098.solivagant.lifecycle.Lifecycle.State +import kotlin.js.JsName +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * Represents [Lifecycle] and [Lifecycle.Observer] at the same time. + * Can be used to manually control the [Lifecycle]. + */ +public interface LifecycleRegistry : Lifecycle, Observer + +/** + * Creates a default implementation of [LifecycleRegistry]. + */ +@JsName("lifecycleRegistry") +public fun LifecycleRegistry(): LifecycleRegistry = LifecycleRegistry(initialState = State.INITIALIZED) + +/** + * Creates a default implementation of [LifecycleRegistry] with the specified [initialState]. + */ +public fun LifecycleRegistry( + initialState: State, +): LifecycleRegistry = LifecycleRegistryImpl(initialState) + +private class LifecycleRegistryImpl(initialState: State) : LifecycleRegistry { + private val _currentStateFlow = MutableStateFlow(initialState) + + private inline var _state: State + get() = _currentStateFlow.value + set(value) { + _currentStateFlow.value = value + } + + private var observers: List = emptyList() + + override val currentStateFlow: StateFlow + get() = _currentStateFlow.asStateFlow() + + override fun onStateChanged(event: Event) { + when (event) { + Event.ON_CREATE -> onCreate() + Event.ON_START -> onStart() + Event.ON_RESUME -> onResume() + Event.ON_PAUSE -> onPause() + Event.ON_STOP -> onStop() + Event.ON_DESTROY -> onDestroy() + } + } + + override fun subscribe(observer: Observer): Cancellable { + observers += observer + + val state = _state + if (state >= State.CREATED) { + observer.onStateChanged(Event.ON_CREATE) + } + if (state >= State.STARTED) { + observer.onStateChanged(Event.ON_START) + } + if (state >= State.RESUMED) { + observer.onStateChanged(Event.ON_RESUME) + } + + return Cancellable { observers -= observer } + } + + private fun onCreate() { + checkState(State.INITIALIZED) + _state = State.CREATED + observers.forEach { it.onStateChanged(Event.ON_CREATE) } + } + + private fun onStart() { + checkState(State.CREATED) + _state = State.STARTED + observers.forEach { it.onStateChanged(Event.ON_START) } + } + + private fun onResume() { + checkState(State.STARTED) + _state = State.RESUMED + observers.forEach { it.onStateChanged(Event.ON_RESUME) } + } + + private fun onPause() { + checkState(State.RESUMED) + _state = State.STARTED + observers.reversed().forEach { it.onStateChanged(Event.ON_PAUSE) } + } + + private fun onStop() { + checkState(State.STARTED) + _state = State.CREATED + observers.asReversed().forEach { it.onStateChanged(Event.ON_STOP) } + } + + private fun onDestroy() { + checkState(State.CREATED) + _state = State.DESTROYED + observers.asReversed().forEach { it.onStateChanged(Event.ON_DESTROY) } + observers = emptyList() + } + + private fun checkState(required: State) { + check(_state == required) { "Expected state $required but was $_state" } + } +} diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/repeatOnLifecycle.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/repeatOnLifecycle.kt new file mode 100644 index 00000000..f6a98b01 --- /dev/null +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/repeatOnLifecycle.kt @@ -0,0 +1,111 @@ +package com.hoc081098.solivagant.lifecycle + +import com.hoc081098.solivagant.lifecycle.Lifecycle.Cancellable +import com.hoc081098.solivagant.lifecycle.Lifecycle.Companion.currentState +import kotlin.coroutines.resume +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext + +/** + * Runs the given [block] in a new coroutine when `this` [Lifecycle] is at least at [state] and + * suspends the execution until `this` [Lifecycle] is [Lifecycle.State.DESTROYED]. + * + * The [block] will cancel and re-launch as the lifecycle moves in and out of the target state. + * + * The best practice is to call this function when the lifecycle is initialized. For + * example, `onCreate` in an Activity, or `onViewCreated` in a Fragment. Otherwise, multiple + * repeating coroutines doing the same could be created and be executed at the same time. + * + * Repeated invocations of `block` will run serially, that is they will always wait for the + * previous invocation to fully finish before re-starting execution as the state moves in and out + * of the required state. + * + * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a + * parameter will throw an [IllegalArgumentException]. + * + * @param state [Lifecycle.State] in which `block` runs in a new coroutine. That coroutine + * will cancel if the lifecycle falls below that state, and will restart if it's in that state + * again. + * @param block The block to run when the lifecycle is at least in [state] state. + */ +public suspend fun Lifecycle.repeatOnLifecycle( + state: Lifecycle.State, + block: suspend CoroutineScope.() -> Unit, +) { + require(state !== Lifecycle.State.INITIALIZED) { + "repeatOnLifecycle cannot start work with the INITIALIZED lifecycle state." + } + + if (currentState === Lifecycle.State.DESTROYED) { + return + } + + // This scope is required to preserve context before we move to Dispatchers.Main + coroutineScope { + withContext(Dispatchers.Main.immediate) { + // Check the current state of the lifecycle as the previous check is not guaranteed + // to be done on the main thread. + if (currentState === Lifecycle.State.DESTROYED) return@withContext + + // Instance of the running repeating coroutine + var launchedJob: Job? = null + + // Registered observer + var closeable: Cancellable? = null + try { + // Suspend the coroutine until the lifecycle is destroyed or + // the coroutine is cancelled + suspendCancellableCoroutine { cont -> + // Lifecycle observers that executes `block` when the lifecycle reaches certain state, and + // cancels when it falls below that state. + val startWorkEvent = Lifecycle.Event.upTo(state) + val cancelWorkEvent = Lifecycle.Event.downFrom(state) + val mutex = Mutex() + closeable = Lifecycle.Observer { event -> + if (event == startWorkEvent) { + // Launch the repeating work preserving the calling context + launchedJob = this@coroutineScope.launch { + // Mutex makes invocations run serially, + // coroutineScope ensures all child coroutines finish + mutex.withLock { + coroutineScope { + block() + } + } + } + return@Observer + } + if (event == cancelWorkEvent) { + launchedJob?.cancel() + launchedJob = null + } + if (event == Lifecycle.Event.ON_DESTROY) { + cont.resume(Unit) + } + }.let { this@repeatOnLifecycle.subscribe(it) } + } + } finally { + launchedJob?.cancel() + closeable?.cancel() + } + } + } +} + +/** + * [LifecycleOwner]'s extension function for [Lifecycle.repeatOnLifecycle] to allow an easier + * call to the API from LifecycleOwners such as Activities and Fragments. + * + * @see Lifecycle.repeatOnLifecycle + */ +public suspend fun LifecycleOwner.repeatOnLifecycle( + state: Lifecycle.State, + block: suspend CoroutineScope.() -> Unit, +): Unit = lifecycle.repeatOnLifecycle(state, block) diff --git a/navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.android.kt b/navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.android.kt new file mode 100644 index 00000000..9e916330 --- /dev/null +++ b/navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.android.kt @@ -0,0 +1,13 @@ +package com.hoc081098.solivagant.navigation.internal + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalLifecycleOwner as AndroidXLocalLifecycleOwner +import com.hoc081098.solivagant.lifecycle.LifecycleOwner +import com.hoc081098.solivagant.lifecycle.asSolivagantLifecycleOwner + +@Composable +internal actual fun rememberPlatformLifecycleOwner(): LifecycleOwner { + val androidXLifecycleOwner = AndroidXLocalLifecycleOwner.current + return remember(androidXLifecycleOwner) { androidXLifecycleOwner.asSolivagantLifecycleOwner() } +} diff --git a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt index 2af2df45..d0ce5891 100644 --- a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt +++ b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt @@ -24,6 +24,8 @@ import com.hoc081098.solivagant.navigation.internal.StackEntryViewModelStoreOwne import com.hoc081098.solivagant.navigation.internal.WeakReference import com.hoc081098.solivagant.navigation.internal.currentBackPressedDispatcher import com.hoc081098.solivagant.navigation.internal.rememberNavigationExecutor +import com.hoc081098.solivagant.navigation.internal.rememberPlatformLifecycleOwner +import com.hoc081098.solivagant.navigation.lifecycle.LocalLifecycleOwner import kotlinx.collections.immutable.ImmutableSet import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -50,7 +52,12 @@ public fun NavHost( DestinationChangedCallback(executor, destinationChangedCallback) val saveableStateHolder = rememberSaveableStateHolder() - CompositionLocalProvider(LocalNavigationExecutor provides executor) { + val lifecycleOwner = rememberPlatformLifecycleOwner() + + CompositionLocalProvider( + LocalNavigationExecutor provides executor, + LocalLifecycleOwner provides lifecycleOwner, + ) { if (navEventNavigator != null) { NavigationSetup(navEventNavigator) } diff --git a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.kt b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.kt new file mode 100644 index 00000000..b7a95809 --- /dev/null +++ b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.kt @@ -0,0 +1,7 @@ +package com.hoc081098.solivagant.navigation.internal + +import androidx.compose.runtime.Composable +import com.hoc081098.solivagant.lifecycle.LifecycleOwner + +@Composable +internal expect fun rememberPlatformLifecycleOwner(): LifecycleOwner diff --git a/settings.gradle.kts b/settings.gradle.kts index 78a8469a..9970bf28 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,6 +23,7 @@ dependencyResolutionManagement { rootProject.name = "solivagant" include(":navigation-core") include(":khonshu-navigation-core") +include(":lifecycle") include(":navigation") include(":sample:app", ":sample:shared") From 645368ba5bb69ad509be7659a1e2b1af09c26b3c Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sat, 20 Jan 2024 14:54:43 +0700 Subject: [PATCH 02/28] add lifecycle module --- .../api/android/khonshu-navigation-core.api | 4 + .../api/jvm/khonshu-navigation-core.api | 4 + .../lifecycleAndroidSupport.nonAndroid.kt | 14 ---- lifecycle/api/android/lifecycle.api | 77 +++++++++++++++++++ lifecycle/api/jvm/lifecycle.api | 70 +++++++++++++++++ .../solivagant/navigation/NavHost.kt | 1 - 6 files changed, 155 insertions(+), 15 deletions(-) delete mode 100644 khonshu-navigation-core/src/nonAndroidMain/kotlin/com/hoc081098/solivagant/navigation/internal/lifecycleAndroidSupport.nonAndroid.kt create mode 100644 lifecycle/api/android/lifecycle.api create mode 100644 lifecycle/api/jvm/lifecycle.api diff --git a/khonshu-navigation-core/api/android/khonshu-navigation-core.api b/khonshu-navigation-core/api/android/khonshu-navigation-core.api index 07b4840b..6ca75f4d 100644 --- a/khonshu-navigation-core/api/android/khonshu-navigation-core.api +++ b/khonshu-navigation-core/api/android/khonshu-navigation-core.api @@ -24,6 +24,10 @@ public final class com/hoc081098/solivagant/navigation/InitialValue$Creator : an public synthetic fun newArray (I)[Ljava/lang/Object; } +public final class com/hoc081098/solivagant/navigation/LocalLifecycleOwnerKt { + public static final fun getLocalLifecycleOwner ()Landroidx/compose/runtime/ProvidableCompositionLocal; +} + public abstract interface class com/hoc081098/solivagant/navigation/NavDestination { } diff --git a/khonshu-navigation-core/api/jvm/khonshu-navigation-core.api b/khonshu-navigation-core/api/jvm/khonshu-navigation-core.api index 0c7010b3..e242903a 100644 --- a/khonshu-navigation-core/api/jvm/khonshu-navigation-core.api +++ b/khonshu-navigation-core/api/jvm/khonshu-navigation-core.api @@ -16,6 +16,10 @@ public final class com/hoc081098/solivagant/navigation/EXTRA_ROUTEKt { public static final field EXTRA_ROUTE Ljava/lang/String; } +public final class com/hoc081098/solivagant/navigation/LocalLifecycleOwnerKt { + public static final fun getLocalLifecycleOwner ()Landroidx/compose/runtime/ProvidableCompositionLocal; +} + public abstract interface class com/hoc081098/solivagant/navigation/NavDestination { } diff --git a/khonshu-navigation-core/src/nonAndroidMain/kotlin/com/hoc081098/solivagant/navigation/internal/lifecycleAndroidSupport.nonAndroid.kt b/khonshu-navigation-core/src/nonAndroidMain/kotlin/com/hoc081098/solivagant/navigation/internal/lifecycleAndroidSupport.nonAndroid.kt deleted file mode 100644 index 175cdcbc..00000000 --- a/khonshu-navigation-core/src/nonAndroidMain/kotlin/com/hoc081098/solivagant/navigation/internal/lifecycleAndroidSupport.nonAndroid.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.hoc081098.solivagant.navigation.internal - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable - -internal actual interface LifecycleOwner { - companion object : LifecycleOwner -} - -@Composable -@ReadOnlyComposable -internal actual fun currentLifecycleOwner(): LifecycleOwner = LifecycleOwner - -internal actual suspend fun LifecycleOwner.repeatOnResumeLifecycle(block: suspend () -> Unit) = block() diff --git a/lifecycle/api/android/lifecycle.api b/lifecycle/api/android/lifecycle.api new file mode 100644 index 00000000..7a02b0f4 --- /dev/null +++ b/lifecycle/api/android/lifecycle.api @@ -0,0 +1,77 @@ +public final class com/hoc081098/solivagant/lifecycle/AndroidKt { + public static final fun asSolivagantEventOrNull (Landroidx/lifecycle/Lifecycle$Event;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static final fun asSolivagantLifecycle (Landroidx/lifecycle/Lifecycle;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle; + public static final fun asSolivagantLifecycleOwner (Landroidx/lifecycle/LifecycleOwner;)Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner; + public static final fun asSolivagantState (Landroidx/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; +} + +public abstract interface class com/hoc081098/solivagant/lifecycle/Lifecycle { + public static final field Companion Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Companion; + public abstract fun getCurrentStateFlow ()Lkotlinx/coroutines/flow/StateFlow; + public abstract fun subscribe (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Observer;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Cancellable; +} + +public abstract interface class com/hoc081098/solivagant/lifecycle/Lifecycle$Cancellable { + public abstract fun cancel ()V +} + +public final class com/hoc081098/solivagant/lifecycle/Lifecycle$Companion { + public final fun getCurrentState (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; +} + +public final class com/hoc081098/solivagant/lifecycle/Lifecycle$Event : java/lang/Enum { + public static final field Companion Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event$Companion; + public static final field ON_CREATE Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static final field ON_DESTROY Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static final field ON_PAUSE Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static final field ON_RESUME Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static final field ON_START Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static final field ON_STOP Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static final fun downFrom (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static final fun downTo (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static final fun upFrom (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static final fun upTo (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static fun valueOf (Ljava/lang/String;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static fun values ()[Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; +} + +public final class com/hoc081098/solivagant/lifecycle/Lifecycle$Event$Companion { + public final fun downFrom (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public final fun downTo (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public final fun upFrom (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public final fun upTo (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; +} + +public abstract interface class com/hoc081098/solivagant/lifecycle/Lifecycle$Observer { + public abstract fun onStateChanged (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event;)V +} + +public final class com/hoc081098/solivagant/lifecycle/Lifecycle$State : java/lang/Enum { + public static final field CREATED Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; + public static final field DESTROYED Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; + public static final field INITIALIZED Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; + public static final field RESUMED Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; + public static final field STARTED Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; + public static fun values ()[Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; +} + +public abstract interface class com/hoc081098/solivagant/lifecycle/LifecycleOwner { + public abstract fun getLifecycle ()Lcom/hoc081098/solivagant/lifecycle/Lifecycle; +} + +public final class com/hoc081098/solivagant/lifecycle/LifecycleOwnerRegistryKt { + public static final fun LifecycleRegistry ()Lcom/hoc081098/solivagant/lifecycle/LifecycleRegistry; + public static final fun LifecycleRegistry (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/LifecycleRegistry; +} + +public abstract interface class com/hoc081098/solivagant/lifecycle/LifecycleRegistry : com/hoc081098/solivagant/lifecycle/Lifecycle, com/hoc081098/solivagant/lifecycle/Lifecycle$Observer { +} + +public final class com/hoc081098/solivagant/lifecycle/RepeatOnLifecycleKt { + public static final fun repeatOnLifecycle (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun repeatOnLifecycle (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + diff --git a/lifecycle/api/jvm/lifecycle.api b/lifecycle/api/jvm/lifecycle.api new file mode 100644 index 00000000..5ee5f33a --- /dev/null +++ b/lifecycle/api/jvm/lifecycle.api @@ -0,0 +1,70 @@ +public abstract interface class com/hoc081098/solivagant/lifecycle/Lifecycle { + public static final field Companion Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Companion; + public abstract fun getCurrentStateFlow ()Lkotlinx/coroutines/flow/StateFlow; + public abstract fun subscribe (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Observer;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Cancellable; +} + +public abstract interface class com/hoc081098/solivagant/lifecycle/Lifecycle$Cancellable { + public abstract fun cancel ()V +} + +public final class com/hoc081098/solivagant/lifecycle/Lifecycle$Companion { + public final fun getCurrentState (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; +} + +public final class com/hoc081098/solivagant/lifecycle/Lifecycle$Event : java/lang/Enum { + public static final field Companion Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event$Companion; + public static final field ON_CREATE Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static final field ON_DESTROY Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static final field ON_PAUSE Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static final field ON_RESUME Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static final field ON_START Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static final field ON_STOP Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static final fun downFrom (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static final fun downTo (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static final fun upFrom (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static final fun upTo (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static fun valueOf (Ljava/lang/String;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public static fun values ()[Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; +} + +public final class com/hoc081098/solivagant/lifecycle/Lifecycle$Event$Companion { + public final fun downFrom (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public final fun downTo (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public final fun upFrom (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; + public final fun upTo (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; +} + +public abstract interface class com/hoc081098/solivagant/lifecycle/Lifecycle$Observer { + public abstract fun onStateChanged (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event;)V +} + +public final class com/hoc081098/solivagant/lifecycle/Lifecycle$State : java/lang/Enum { + public static final field CREATED Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; + public static final field DESTROYED Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; + public static final field INITIALIZED Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; + public static final field RESUMED Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; + public static final field STARTED Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; + public static fun values ()[Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; +} + +public abstract interface class com/hoc081098/solivagant/lifecycle/LifecycleOwner { + public abstract fun getLifecycle ()Lcom/hoc081098/solivagant/lifecycle/Lifecycle; +} + +public final class com/hoc081098/solivagant/lifecycle/LifecycleOwnerRegistryKt { + public static final fun LifecycleRegistry ()Lcom/hoc081098/solivagant/lifecycle/LifecycleRegistry; + public static final fun LifecycleRegistry (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/LifecycleRegistry; +} + +public abstract interface class com/hoc081098/solivagant/lifecycle/LifecycleRegistry : com/hoc081098/solivagant/lifecycle/Lifecycle, com/hoc081098/solivagant/lifecycle/Lifecycle$Observer { +} + +public final class com/hoc081098/solivagant/lifecycle/RepeatOnLifecycleKt { + public static final fun repeatOnLifecycle (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun repeatOnLifecycle (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + diff --git a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt index d0ce5891..4fe0a280 100644 --- a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt +++ b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt @@ -25,7 +25,6 @@ import com.hoc081098.solivagant.navigation.internal.WeakReference import com.hoc081098.solivagant.navigation.internal.currentBackPressedDispatcher import com.hoc081098.solivagant.navigation.internal.rememberNavigationExecutor import com.hoc081098.solivagant.navigation.internal.rememberPlatformLifecycleOwner -import com.hoc081098.solivagant.navigation.lifecycle.LocalLifecycleOwner import kotlinx.collections.immutable.ImmutableSet import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map From 3e92daaa3229071f203e481c0f160b79319e9606 Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sat, 20 Jan 2024 15:45:34 +0700 Subject: [PATCH 03/28] add lifecycle module --- .../internal/lifecycleAndroidSupport.ios.kt | 51 ---------- navigation/build.gradle.kts | 21 ++++ ...memberPlatformLifecycleOwner.iosAndTvOs.kt | 98 +++++++++++++++++++ .../rememberPlatformLifecycleOwner.js.kt | 16 +++ .../rememberPlatformLifecycleOwner.jvm.kt | 39 ++++++++ 5 files changed, 174 insertions(+), 51 deletions(-) delete mode 100644 khonshu-navigation-core/src/iosMain/kotlin/com/hoc081098/solivagant/navigation/internal/lifecycleAndroidSupport.ios.kt create mode 100644 navigation/src/iosAndTvOs/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.iosAndTvOs.kt create mode 100644 navigation/src/jsMain/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.js.kt create mode 100644 navigation/src/jvmMain/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.jvm.kt diff --git a/khonshu-navigation-core/src/iosMain/kotlin/com/hoc081098/solivagant/navigation/internal/lifecycleAndroidSupport.ios.kt b/khonshu-navigation-core/src/iosMain/kotlin/com/hoc081098/solivagant/navigation/internal/lifecycleAndroidSupport.ios.kt deleted file mode 100644 index fadfb9ea..00000000 --- a/khonshu-navigation-core/src/iosMain/kotlin/com/hoc081098/solivagant/navigation/internal/lifecycleAndroidSupport.ios.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.hoc081098.solivagant.navigation.internal - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.ui.interop.LocalUIViewController -import platform.Foundation.NSNotification -import platform.Foundation.NSNotificationCenter -import platform.Foundation.NSNotificationName -import platform.Foundation.NSOperationQueue -import platform.UIKit.UIApplication -import platform.UIKit.UIApplicationDidBecomeActiveNotification -import platform.UIKit.UIApplicationDidEnterBackgroundNotification -import platform.UIKit.UIApplicationWillEnterForegroundNotification -import platform.UIKit.UIApplicationWillResignActiveNotification -import platform.UIKit.UIApplicationWillTerminateNotification -import platform.UIKit.UIViewController -import platform.darwin.NSObjectProtocol - -internal actual interface LifecycleOwner - -private class LifecycleOwnerImpl( - private val uiViewController: UIViewController, -) : LifecycleOwner { - - private val willEnterForegroundObserver = addObserver(UIApplicationWillEnterForegroundNotification) { lifecycle.start() } - private val didBecomeActiveObserver = addObserver(UIApplicationDidBecomeActiveNotification) { lifecycle.resume() } - private val willResignActiveObserver = addObserver(UIApplicationWillResignActiveNotification) { lifecycle.pause() } - private val didEnterBackgroundObserver = addObserver(UIApplicationDidEnterBackgroundNotification) { lifecycle.stop() } - private val willTerminateObserver = addObserver(UIApplicationWillTerminateNotification) { lifecycle.destroy() } - - init { - } -} - -@Composable -@ReadOnlyComposable -internal actual fun currentLifecycleOwner(): LifecycleOwner = LifecycleOwnerImpl( - uiViewController = LocalUIViewController.current, -) - -internal actual suspend fun LifecycleOwner.repeatOnResumeLifecycle(block: suspend () -> Unit) { - UIApplication.sharedApplication().delegate -} - -private fun addObserver(name: NSNotificationName, block: (NSNotification?) -> Unit): NSObjectProtocol = - NSNotificationCenter.defaultCenter.addObserverForName( - name = name, - `object` = null, - queue = NSOperationQueue.mainQueue, - usingBlock = block, - ) diff --git a/navigation/build.gradle.kts b/navigation/build.gradle.kts index 844cab6f..21250a35 100644 --- a/navigation/build.gradle.kts +++ b/navigation/build.gradle.kts @@ -164,6 +164,27 @@ kotlin { dependsOn(nonAndroidTest) dependsOn(nonJvmTest) } + + val iosAndTvOs by creating { + dependsOn(appleMain.get()) + } + val iosAndTvOsTest by creating { + dependsOn(appleTest.get()) + } + + iosMain { + dependsOn(iosAndTvOs) + } + iosTest { + dependsOn(iosAndTvOsTest) + } + + tvosMain { + dependsOn(iosAndTvOs) + } + tvosTest { + dependsOn(iosAndTvOsTest) + } } sourceSets.matching { it.name.contains("Test") }.all { diff --git a/navigation/src/iosAndTvOs/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.iosAndTvOs.kt b/navigation/src/iosAndTvOs/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.iosAndTvOs.kt new file mode 100644 index 00000000..79ca155c --- /dev/null +++ b/navigation/src/iosAndTvOs/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.iosAndTvOs.kt @@ -0,0 +1,98 @@ +package com.hoc081098.solivagant.navigation.internal + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.hoc081098.solivagant.lifecycle.Lifecycle +import com.hoc081098.solivagant.lifecycle.Lifecycle.Companion.currentState +import com.hoc081098.solivagant.lifecycle.LifecycleOwner +import com.hoc081098.solivagant.lifecycle.LifecycleRegistry +import platform.Foundation.NSNotification +import platform.Foundation.NSNotificationCenter +import platform.Foundation.NSNotificationName +import platform.Foundation.NSOperationQueue +import platform.UIKit.UIApplication +import platform.UIKit.UIApplicationDidBecomeActiveNotification +import platform.UIKit.UIApplicationDidEnterBackgroundNotification +import platform.UIKit.UIApplicationState +import platform.UIKit.UIApplicationWillEnterForegroundNotification +import platform.UIKit.UIApplicationWillResignActiveNotification +import platform.UIKit.UIApplicationWillTerminateNotification +import platform.darwin.NSObjectProtocol + +private class AppLifecycleOwner : LifecycleOwner { + private val lifecycleRegistry = LifecycleRegistry() + override val lifecycle: Lifecycle get() = lifecycleRegistry + + // A notification that posts shortly before an app leaves the background state on its way to becoming the active app. + private val willEnterForegroundObserver = addObserver(UIApplicationWillEnterForegroundNotification) { + lifecycleRegistry.onStateChanged(Lifecycle.Event.ON_START) + } + + // A notification that posts when the app becomes active. + private val didBecomeActiveObserver = addObserver(UIApplicationDidBecomeActiveNotification) { + lifecycleRegistry.onStateChanged(Lifecycle.Event.ON_RESUME) + } + + // A notification that posts when the app is no longer active and loses focus. + private val willResignActiveObserver = addObserver(UIApplicationWillResignActiveNotification) { + lifecycleRegistry.onStateChanged(Lifecycle.Event.ON_PAUSE) + } + + // A notification that posts when the app enters the background. + private val didEnterBackgroundObserver = addObserver(UIApplicationDidEnterBackgroundNotification) { + lifecycleRegistry.onStateChanged(Lifecycle.Event.ON_STOP) + } + + // Tells the delegate when the app is about to terminate. + private val willTerminateObserver = addObserver(UIApplicationWillTerminateNotification) { + lifecycleRegistry.onStateChanged(Lifecycle.Event.ON_DESTROY) + } + + init { + NSOperationQueue.mainQueue.addOperationWithBlock { + if (lifecycle.currentState == Lifecycle.State.INITIALIZED) { + when (UIApplication.sharedApplication.applicationState) { + UIApplicationState.UIApplicationStateActive -> + // The app is running in the foreground and currently receiving events. + lifecycleRegistry.onStateChanged(Lifecycle.Event.ON_RESUME) + + UIApplicationState.UIApplicationStateInactive -> + // The app is running in the foreground but isn’t receiving events. + lifecycleRegistry.onStateChanged(Lifecycle.Event.ON_START) + + UIApplicationState.UIApplicationStateBackground -> + // The app is running in the background. + lifecycleRegistry.onStateChanged(Lifecycle.Event.ON_CREATE) + + else -> lifecycleRegistry.onStateChanged(Lifecycle.Event.ON_CREATE) + } + } + } + + lifecycleRegistry.subscribe { event -> + if (event == Lifecycle.Event.ON_DESTROY) { + removeObserver(willEnterForegroundObserver) + removeObserver(didBecomeActiveObserver) + removeObserver(willResignActiveObserver) + removeObserver(didEnterBackgroundObserver) + removeObserver(willTerminateObserver) + } + } + } +} + +private fun addObserver(name: NSNotificationName, block: (NSNotification?) -> Unit): NSObjectProtocol = + NSNotificationCenter.defaultCenter.addObserverForName( + name = name, + `object` = null, + queue = NSOperationQueue.mainQueue, + usingBlock = block, + ) + +private fun removeObserver(observer: NSObjectProtocol) = + NSNotificationCenter.defaultCenter.removeObserver(observer) + +@Composable +internal actual fun rememberPlatformLifecycleOwner(): LifecycleOwner { + return remember { AppLifecycleOwner() } +} diff --git a/navigation/src/jsMain/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.js.kt b/navigation/src/jsMain/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.js.kt new file mode 100644 index 00000000..edec0a99 --- /dev/null +++ b/navigation/src/jsMain/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.js.kt @@ -0,0 +1,16 @@ +package com.hoc081098.solivagant.navigation.internal + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.hoc081098.solivagant.lifecycle.Lifecycle +import com.hoc081098.solivagant.lifecycle.LifecycleOwner +import com.hoc081098.solivagant.lifecycle.LifecycleRegistry + +private class DefaultJsLifecycleOwner : LifecycleOwner { + override val lifecycle: Lifecycle = LifecycleRegistry(initialState = Lifecycle.State.RESUMED) +} + +@Composable +internal actual fun rememberPlatformLifecycleOwner(): LifecycleOwner { + return remember { DefaultJsLifecycleOwner() } +} diff --git a/navigation/src/jvmMain/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.jvm.kt b/navigation/src/jvmMain/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.jvm.kt new file mode 100644 index 00000000..3db5c7ba --- /dev/null +++ b/navigation/src/jvmMain/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.jvm.kt @@ -0,0 +1,39 @@ +package com.hoc081098.solivagant.navigation.internal + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.window.WindowState +import com.hoc081098.solivagant.lifecycle.Lifecycle +import com.hoc081098.solivagant.lifecycle.LifecycleOwner +import com.hoc081098.solivagant.lifecycle.LifecycleRegistry + +@Composable +public fun LifecycleControllerEffect(lifecycleRegistry: LifecycleRegistry, windowState: WindowState) { + LaunchedEffect(lifecycleRegistry, windowState) { + snapshotFlow { windowState.isMinimized } + .collect { isMinimized -> + if (isMinimized) { + lifecycleRegistry.onStateChanged(Lifecycle.Event.ON_STOP) + } else { + lifecycleRegistry.onStateChanged(Lifecycle.Event.ON_RESUME) + } + } + } + + DisposableEffect(lifecycleRegistry) { + lifecycleRegistry.onStateChanged(Lifecycle.Event.ON_CREATE) + onDispose { lifecycleRegistry.onStateChanged(Lifecycle.Event.ON_DESTROY) } + } +} + +private class DefaultDesktopLifecycleOwner : LifecycleOwner { + override val lifecycle: Lifecycle = LifecycleRegistry(initialState = Lifecycle.State.RESUMED) +} + +@Composable +internal actual fun rememberPlatformLifecycleOwner(): LifecycleOwner { + return remember { DefaultDesktopLifecycleOwner() } +} From 7a1c03b94981dcd5791d8d0d4dfea318774c77d0 Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sat, 20 Jan 2024 16:00:42 +0700 Subject: [PATCH 04/28] add lifecycle module --- .../rememberPlatformLifecycleOwner.macos.kt | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 navigation/src/macosMain/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.macos.kt diff --git a/navigation/src/macosMain/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.macos.kt b/navigation/src/macosMain/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.macos.kt new file mode 100644 index 00000000..f9d222bf --- /dev/null +++ b/navigation/src/macosMain/kotlin/com/hoc081098/solivagant/navigation/internal/rememberPlatformLifecycleOwner.macos.kt @@ -0,0 +1,82 @@ +package com.hoc081098.solivagant.navigation.internal + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.hoc081098.solivagant.lifecycle.Lifecycle +import com.hoc081098.solivagant.lifecycle.Lifecycle.Companion.currentState +import com.hoc081098.solivagant.lifecycle.LifecycleOwner +import com.hoc081098.solivagant.lifecycle.LifecycleRegistry +import platform.AppKit.NSApplication +import platform.AppKit.NSApplicationDidBecomeActiveNotification +import platform.AppKit.NSApplicationDidHideNotification +import platform.AppKit.NSApplicationWillResignActiveNotification +import platform.AppKit.NSApplicationWillTerminateNotification +import platform.AppKit.NSApplicationWillUnhideNotification +import platform.Foundation.NSNotification +import platform.Foundation.NSNotificationCenter +import platform.Foundation.NSNotificationName +import platform.Foundation.NSOperationQueue +import platform.darwin.NSObjectProtocol + +private class AppLifecycleOwner : LifecycleOwner { + private val lifecycleRegistry = LifecycleRegistry() + override val lifecycle: Lifecycle get() = lifecycleRegistry + + private val willUnHideObserver = addObserver(NSApplicationWillUnhideNotification) { + lifecycleRegistry.onStateChanged(Lifecycle.Event.ON_START) + } + + private val didBecomeActiveObserver = addObserver(NSApplicationDidBecomeActiveNotification) { + lifecycleRegistry.onStateChanged(Lifecycle.Event.ON_RESUME) + } + + private val willResignActiveObserver = addObserver(NSApplicationWillResignActiveNotification) { + lifecycleRegistry.onStateChanged(Lifecycle.Event.ON_PAUSE) + } + + private val didHideObserver = addObserver(NSApplicationDidHideNotification) { + lifecycleRegistry.onStateChanged(Lifecycle.Event.ON_STOP) + } + + private val willTerminateObserver = addObserver(NSApplicationWillTerminateNotification) { + lifecycleRegistry.onStateChanged(Lifecycle.Event.ON_DESTROY) + } + + init { + NSOperationQueue.mainQueue.addOperationWithBlock { + if (lifecycle.currentState == Lifecycle.State.INITIALIZED) { + if (NSApplication.sharedApplication.active) { + lifecycleRegistry.onStateChanged(Lifecycle.Event.ON_RESUME) + } else { + lifecycleRegistry.onStateChanged(Lifecycle.Event.ON_START) + } + } + } + + lifecycleRegistry.subscribe { event -> + if (event == Lifecycle.Event.ON_DESTROY) { + removeObserver(willUnHideObserver) + removeObserver(didBecomeActiveObserver) + removeObserver(willResignActiveObserver) + removeObserver(didHideObserver) + removeObserver(willTerminateObserver) + } + } + } +} + +private fun addObserver(name: NSNotificationName, block: (NSNotification?) -> Unit): NSObjectProtocol = + NSNotificationCenter.defaultCenter.addObserverForName( + name = name, + `object` = null, + queue = NSOperationQueue.mainQueue, + usingBlock = block, + ) + +private fun removeObserver(observer: NSObjectProtocol) = + NSNotificationCenter.defaultCenter.removeObserver(observer) + +@Composable +internal actual fun rememberPlatformLifecycleOwner(): LifecycleOwner { + return remember { AppLifecycleOwner() } +} From 0f0c08b2e5fbec722b8463d7f3de5d30a839a43b Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sat, 20 Jan 2024 16:11:43 +0700 Subject: [PATCH 05/28] add lifecycle module --- .../navigation/LocalLifecycleOwner.kt | 35 +++++++++++++++++-- .../navigation/NavEventNavigator.kt | 6 ++-- .../solivagant/navigation/NavHost.kt | 2 +- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt index f6637097..f6c6cd3c 100644 --- a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt +++ b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt @@ -1,12 +1,41 @@ package com.hoc081098.solivagant.navigation -import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ProvidedValue +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.staticCompositionLocalOf import com.hoc081098.solivagant.lifecycle.LifecycleOwner +import com.hoc081098.solivagant.navigation.internal.InternalNavigationApi /** * The CompositionLocal containing the current [LifecycleOwner]. */ -public val LocalLifecycleOwner: ProvidableCompositionLocal = staticCompositionLocalOf { - error("CompositionLocal LocalLifecycleOwner not present") +public object LocalLifecycleOwner { + /** + * The CompositionLocal containing the current [LifecycleOwner]. + */ + private val LocalLifecycleOwner = staticCompositionLocalOf { + error("No LifecycleOwner was provided") + } + + /** + * Returns current composition local value for the owner. + * @throws IllegalStateException if no value was provided. + */ + public val current: LifecycleOwner + @Composable + @ReadOnlyComposable + get() = LocalLifecycleOwner.current + + /** + * Associates a [LocalLifecycleOwner] key to a value in a call to + * [CompositionLocalProvider]. + */ + public infix fun provides(lifecycleOwner: LifecycleOwner): ProvidedValue = + LocalLifecycleOwner.provides(lifecycleOwner) + + @InternalNavigationApi + public infix fun providesDefault(lifecycleOwner: LifecycleOwner): ProvidedValue = + LocalLifecycleOwner.providesDefault(lifecycleOwner) } diff --git a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavEventNavigator.kt b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavEventNavigator.kt index cce36748..cc42858c 100644 --- a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavEventNavigator.kt +++ b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavEventNavigator.kt @@ -132,7 +132,8 @@ public open class NavEventNavigator : Navigator, ResultNavigator, BackIntercepto * Note: You must call this before [NavigationSetup] is called with this navigator." */ protected inline fun registerForNavigationResult(): NavigationResultRequest { - return registerForNavigationResult(DestinationId(T::class), O::class.qualifiedName!!) + // TODO(js): cannot use qualifiedName + return registerForNavigationResult(DestinationId(T::class), O::class.simpleName!!) } @PublishedApi @@ -141,7 +142,8 @@ public open class NavEventNavigator : Navigator, ResultNavigator, BackIntercepto resultType: String, ): NavigationResultRequest { checkAllowedToAddRequests() - val requestKey = "${id.route.qualifiedName!!}-$resultType" + // TODO(js): cannot use qualifiedName + val requestKey = "${id.route.simpleName!!}-$resultType" val key = NavigationResultRequest.Key(id, requestKey) val request = NavigationResultRequest(key) _navigationResultRequests.add(request) diff --git a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt index 4fe0a280..d08b5c36 100644 --- a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt +++ b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt @@ -55,7 +55,7 @@ public fun NavHost( CompositionLocalProvider( LocalNavigationExecutor provides executor, - LocalLifecycleOwner provides lifecycleOwner, + LocalLifecycleOwner providesDefault lifecycleOwner, ) { if (navEventNavigator != null) { NavigationSetup(navEventNavigator) From b9b1e827391b65d26010b6cc24e7047e244fcd7f Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sat, 20 Jan 2024 16:12:10 +0700 Subject: [PATCH 06/28] add lifecycle module --- .../api/android/khonshu-navigation-core.api | 8 ++++++-- .../api/jvm/khonshu-navigation-core.api | 8 ++++++-- navigation/api/jvm/navigation.api | 4 ++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/khonshu-navigation-core/api/android/khonshu-navigation-core.api b/khonshu-navigation-core/api/android/khonshu-navigation-core.api index 6ca75f4d..773eb007 100644 --- a/khonshu-navigation-core/api/android/khonshu-navigation-core.api +++ b/khonshu-navigation-core/api/android/khonshu-navigation-core.api @@ -24,8 +24,12 @@ public final class com/hoc081098/solivagant/navigation/InitialValue$Creator : an public synthetic fun newArray (I)[Ljava/lang/Object; } -public final class com/hoc081098/solivagant/navigation/LocalLifecycleOwnerKt { - public static final fun getLocalLifecycleOwner ()Landroidx/compose/runtime/ProvidableCompositionLocal; +public final class com/hoc081098/solivagant/navigation/LocalLifecycleOwner { + public static final field $stable I + public static final field INSTANCE Lcom/hoc081098/solivagant/navigation/LocalLifecycleOwner; + public final fun getCurrent (Landroidx/compose/runtime/Composer;I)Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner; + public final fun provides (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;)Landroidx/compose/runtime/ProvidedValue; + public final fun providesDefault (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;)Landroidx/compose/runtime/ProvidedValue; } public abstract interface class com/hoc081098/solivagant/navigation/NavDestination { diff --git a/khonshu-navigation-core/api/jvm/khonshu-navigation-core.api b/khonshu-navigation-core/api/jvm/khonshu-navigation-core.api index e242903a..0b3c1d62 100644 --- a/khonshu-navigation-core/api/jvm/khonshu-navigation-core.api +++ b/khonshu-navigation-core/api/jvm/khonshu-navigation-core.api @@ -16,8 +16,12 @@ public final class com/hoc081098/solivagant/navigation/EXTRA_ROUTEKt { public static final field EXTRA_ROUTE Ljava/lang/String; } -public final class com/hoc081098/solivagant/navigation/LocalLifecycleOwnerKt { - public static final fun getLocalLifecycleOwner ()Landroidx/compose/runtime/ProvidableCompositionLocal; +public final class com/hoc081098/solivagant/navigation/LocalLifecycleOwner { + public static final field $stable I + public static final field INSTANCE Lcom/hoc081098/solivagant/navigation/LocalLifecycleOwner; + public final fun getCurrent (Landroidx/compose/runtime/Composer;I)Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner; + public final fun provides (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;)Landroidx/compose/runtime/ProvidedValue; + public final fun providesDefault (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;)Landroidx/compose/runtime/ProvidedValue; } public abstract interface class com/hoc081098/solivagant/navigation/NavDestination { diff --git a/navigation/api/jvm/navigation.api b/navigation/api/jvm/navigation.api index 11a71dee..f982f497 100644 --- a/navigation/api/jvm/navigation.api +++ b/navigation/api/jvm/navigation.api @@ -2,3 +2,7 @@ public final class com/hoc081098/solivagant/navigation/NavHostKt { public static final fun NavHost (Lcom/hoc081098/solivagant/navigation/NavRoot;Lkotlinx/collections/immutable/ImmutableSet;Landroidx/compose/ui/Modifier;Lcom/hoc081098/solivagant/navigation/NavEventNavigator;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V } +public final class com/hoc081098/solivagant/navigation/internal/RememberPlatformLifecycleOwner_jvmKt { + public static final fun LifecycleControllerEffect (Lcom/hoc081098/solivagant/lifecycle/LifecycleRegistry;Landroidx/compose/ui/window/WindowState;Landroidx/compose/runtime/Composer;I)V +} + From d0d592580caf8cb198fbaad5feac95840ef5ed42 Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sat, 20 Jan 2024 16:12:55 +0700 Subject: [PATCH 07/28] add lifecycle module --- .../com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt index f6c6cd3c..cb8d3d0a 100644 --- a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt +++ b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ProvidedValue import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.Stable import androidx.compose.runtime.staticCompositionLocalOf import com.hoc081098.solivagant.lifecycle.LifecycleOwner import com.hoc081098.solivagant.navigation.internal.InternalNavigationApi @@ -11,6 +12,7 @@ import com.hoc081098.solivagant.navigation.internal.InternalNavigationApi /** * The CompositionLocal containing the current [LifecycleOwner]. */ +@Stable public object LocalLifecycleOwner { /** * The CompositionLocal containing the current [LifecycleOwner]. From 503e17fae0dd212053198cb2e0df028ab9aa87e7 Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sat, 20 Jan 2024 16:20:21 +0700 Subject: [PATCH 08/28] detekt --- .../com/hoc081098/solivagant/navigation/EXTRA_ROUTE.kt | 1 + .../hoc081098/solivagant/navigation/LocalLifecycleOwner.kt | 3 ++- .../hoc081098/solivagant/navigation/NavEventNavigator.kt | 6 +++++- .../com/hoc081098/solivagant/navigation/NavigationSetup.kt | 1 + .../com/hoc081098/solivagant/navigation/core/NavHost.kt | 1 + .../internal/savedStateHandleAndroidSupport.android.kt | 1 + .../hoc081098/solivagant/navigation/internal/MultiStack.kt | 2 ++ .../navigation/internal/MultiStackNavigationExecutor.kt | 3 ++- .../internal/MultiStackNavigationExecutorBuilder.kt | 2 ++ .../com/hoc081098/solivagant/navigation/internal/Stack.kt | 4 +++- .../com/hoc081098/solivagant/sample/SolivagantSampleApp.kt | 1 + .../sample/product_detail/ProductDetailScreenRoute.kt | 1 + .../solivagant/sample/product_detail/ProductDetailState.kt | 1 + .../sample/search_products/SearchProductScreenRoute.kt | 1 + .../sample/search_products/SearchProductsState.kt | 1 + 15 files changed, 25 insertions(+), 4 deletions(-) diff --git a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/EXTRA_ROUTE.kt b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/EXTRA_ROUTE.kt index d8f9600f..076a6b55 100644 --- a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/EXTRA_ROUTE.kt +++ b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/EXTRA_ROUTE.kt @@ -5,4 +5,5 @@ import com.hoc081098.solivagant.navigation.internal.InternalNavigationApi @InternalNavigationApi @Stable +@Suppress("TopLevelPropertyNaming") public const val EXTRA_ROUTE: String = "com.hoc081098.solivagant.navigation.ROUTE" diff --git a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt index cb8d3d0a..bac313dc 100644 --- a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt +++ b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt @@ -17,6 +17,7 @@ public object LocalLifecycleOwner { /** * The CompositionLocal containing the current [LifecycleOwner]. */ + @Suppress("CompositionLocalAllowlist", "MemberNameEqualsClassName") private val LocalLifecycleOwner = staticCompositionLocalOf { error("No LifecycleOwner was provided") } @@ -31,7 +32,7 @@ public object LocalLifecycleOwner { get() = LocalLifecycleOwner.current /** - * Associates a [LocalLifecycleOwner] key to a value in a call to + * Associates a [lifecycleOwnerProvidableCompositionLocal] key to a value in a call to * [CompositionLocalProvider]. */ public infix fun provides(lifecycleOwner: LifecycleOwner): ProvidedValue = diff --git a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavEventNavigator.kt b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavEventNavigator.kt index cc42858c..79e7c273 100644 --- a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavEventNavigator.kt +++ b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavEventNavigator.kt @@ -112,6 +112,7 @@ public interface BackInterceptor { * the correct events were emitted. */ @MainThread +@Suppress("TooManyFunctions") public open class NavEventNavigator : Navigator, ResultNavigator, BackInterceptor { private val _navEvents = Channel(Channel.UNLIMITED) @@ -131,7 +132,10 @@ public open class NavEventNavigator : Navigator, ResultNavigator, BackIntercepto * * Note: You must call this before [NavigationSetup] is called with this navigator." */ - protected inline fun registerForNavigationResult(): NavigationResultRequest { + protected inline fun < + reified T : BaseRoute, + reified O : Parcelable + > registerForNavigationResult(): NavigationResultRequest { // TODO(js): cannot use qualifiedName return registerForNavigationResult(DestinationId(T::class), O::class.simpleName!!) } diff --git a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavigationSetup.kt b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavigationSetup.kt index d449c267..68ff472a 100644 --- a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavigationSetup.kt +++ b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavigationSetup.kt @@ -131,6 +131,7 @@ internal suspend fun NavigationExecutor.collectAndHandleNavigat @Parcelize private object InitialValue : Parcelable +@Suppress("CompositionLocalAllowlist") @InternalNavigationApi public val LocalNavigationExecutor: ProvidableCompositionLocal = staticCompositionLocalOf { throw IllegalStateException("Can't use NavEventNavigationHandler outside of a navigator NavHost") diff --git a/navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/core/NavHost.kt b/navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/core/NavHost.kt index fcda59e4..ece43e3f 100644 --- a/navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/core/NavHost.kt +++ b/navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/core/NavHost.kt @@ -44,6 +44,7 @@ public fun NavHost( ) { val saveableStateHolder = rememberSaveableStateHolder() + @Suppress("ViewModelInjection") val navStoreViewModel = kmpViewModel( factory = viewModelFactory { NavStoreViewModel( diff --git a/navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/savedStateHandleAndroidSupport.android.kt b/navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/savedStateHandleAndroidSupport.android.kt index d8b7d6fd..c6ca1a4b 100644 --- a/navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/savedStateHandleAndroidSupport.android.kt +++ b/navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/savedStateHandleAndroidSupport.android.kt @@ -31,6 +31,7 @@ internal actual fun SavedStateHandle.setSavedStateProvider( } } +@Suppress("NestedBlockDepth") private fun Map.toBundle(): Bundle = Bundle().apply { forEach { (k, v) -> when (v) { diff --git a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/MultiStack.kt b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/MultiStack.kt index 78d68e1b..15850108 100644 --- a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/MultiStack.kt +++ b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/MultiStack.kt @@ -9,6 +9,7 @@ import com.hoc081098.solivagant.navigation.NavRoot import com.hoc081098.solivagant.navigation.NavRoute import kotlinx.collections.immutable.ImmutableList +@Suppress("TooManyFunctions") internal class MultiStack( // Use ArrayList to make sure it is a RandomAccess private val allStacks: ArrayList, @@ -31,6 +32,7 @@ internal class MultiStack( val startRoot = startStack.rootEntry.route as NavRoot + @Suppress("ReturnCount") fun entryFor(destinationId: DestinationId): StackEntry? { val entry = currentStack.entryFor(destinationId) if (entry != null) { diff --git a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/MultiStackNavigationExecutor.kt b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/MultiStackNavigationExecutor.kt index 72299f1a..4dbe9deb 100644 --- a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/MultiStackNavigationExecutor.kt +++ b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/MultiStackNavigationExecutor.kt @@ -9,6 +9,7 @@ import com.hoc081098.solivagant.navigation.NavRoute import com.hoc081098.solivagant.navigation.Serializable import kotlinx.collections.immutable.ImmutableList +@Suppress("TooManyFunctions") internal class MultiStackNavigationExecutor( private val stack: MultiStack, private val viewModel: StoreViewModel, @@ -89,7 +90,7 @@ internal class MultiStackNavigationExecutor( private fun entryFor(destinationId: DestinationId): StackEntry { return stack.entryFor(destinationId) - ?: throw IllegalStateException("Route $destinationId not found on back stack") + ?: error("Route $destinationId not found on back stack") } internal companion object { diff --git a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/MultiStackNavigationExecutorBuilder.kt b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/MultiStackNavigationExecutorBuilder.kt index 0fa78e5d..55ddc6a6 100644 --- a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/MultiStackNavigationExecutorBuilder.kt +++ b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/MultiStackNavigationExecutorBuilder.kt @@ -9,11 +9,13 @@ import com.hoc081098.solivagant.navigation.ContentDestination import com.hoc081098.solivagant.navigation.NavDestination import com.hoc081098.solivagant.navigation.NavRoot +@Suppress("UnstableCollections") @Composable internal fun rememberNavigationExecutor( startRoot: NavRoot, destinations: Set, ): MultiStackNavigationExecutor { + @Suppress("ViewModelInjection") val viewModel = kmpViewModel( viewModelFactory { StoreViewModel( diff --git a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/Stack.kt b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/Stack.kt index 0a07f25b..a8bd80cf 100644 --- a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/Stack.kt +++ b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/Stack.kt @@ -16,7 +16,8 @@ internal class Stack private constructor( private val onStackEntryRemoved: (StackEntry.Id) -> Unit, private val idGenerator: () -> String, ) { - private val stack = ArrayDeque>(20).also { + @Suppress("MemberNameEqualsClassName") + private val stack = ArrayDeque>(@Suppress("MagicNumber") 20).also { it.addAll(initialStack) } @@ -29,6 +30,7 @@ internal class Stack private constructor( return stack.findLast { it.destinationId == destinationId } as StackEntry? } + @Suppress("NestedBlockDepth") fun computeVisibleEntries(): ImmutableList> { if (stack.size == 1) { return persistentListOf(stack.single()) diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt index 4b57a522..86022c50 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt @@ -47,6 +47,7 @@ import org.koin.compose.koinInject ExperimentalCoilApi::class, ) @Composable +@Suppress("LongMethod") fun SolivagantSampleApp( modifier: Modifier = Modifier, navigator: NavEventNavigator = koinInject(), diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailScreenRoute.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailScreenRoute.kt index c43c29b0..4a6a1f20 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailScreenRoute.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailScreenRoute.kt @@ -1,3 +1,4 @@ +@file:Suppress("PackageNaming") package com.hoc081098.solivagant.sample.product_detail import androidx.compose.runtime.Immutable diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailState.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailState.kt index b9989079..af0f3556 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailState.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailState.kt @@ -1,3 +1,4 @@ +@file:Suppress("PackageNaming") package com.hoc081098.solivagant.sample.product_detail import androidx.compose.runtime.Immutable diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductScreenRoute.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductScreenRoute.kt index 39e9e8ba..2bbbde4a 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductScreenRoute.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductScreenRoute.kt @@ -1,3 +1,4 @@ +@file:Suppress("PackageNaming") package com.hoc081098.solivagant.sample.search_products import androidx.compose.runtime.Immutable diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductsState.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductsState.kt index ed6405fc..d2fa6bd7 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductsState.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductsState.kt @@ -1,3 +1,4 @@ +@file:Suppress("PackageNaming") package com.hoc081098.solivagant.sample.search_products import androidx.compose.runtime.Immutable From 2d2076087b1a6a340000a1b638bf45e1e673778a Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sat, 20 Jan 2024 16:20:44 +0700 Subject: [PATCH 09/28] format --- .../hoc081098/solivagant/navigation/NavEventNavigator.kt | 6 +++--- .../sample/product_detail/ProductDetailScreenRoute.kt | 1 + .../solivagant/sample/product_detail/ProductDetailState.kt | 1 + .../sample/search_products/SearchProductScreenRoute.kt | 1 + .../sample/search_products/SearchProductsState.kt | 1 + 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavEventNavigator.kt b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavEventNavigator.kt index 79e7c273..819901f3 100644 --- a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavEventNavigator.kt +++ b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavEventNavigator.kt @@ -133,9 +133,9 @@ public open class NavEventNavigator : Navigator, ResultNavigator, BackIntercepto * Note: You must call this before [NavigationSetup] is called with this navigator." */ protected inline fun < - reified T : BaseRoute, - reified O : Parcelable - > registerForNavigationResult(): NavigationResultRequest { + reified T : BaseRoute, + reified O : Parcelable, + > registerForNavigationResult(): NavigationResultRequest { // TODO(js): cannot use qualifiedName return registerForNavigationResult(DestinationId(T::class), O::class.simpleName!!) } diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailScreenRoute.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailScreenRoute.kt index 4a6a1f20..87b2ad95 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailScreenRoute.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailScreenRoute.kt @@ -1,4 +1,5 @@ @file:Suppress("PackageNaming") + package com.hoc081098.solivagant.sample.product_detail import androidx.compose.runtime.Immutable diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailState.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailState.kt index af0f3556..bc35b446 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailState.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailState.kt @@ -1,4 +1,5 @@ @file:Suppress("PackageNaming") + package com.hoc081098.solivagant.sample.product_detail import androidx.compose.runtime.Immutable diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductScreenRoute.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductScreenRoute.kt index 2bbbde4a..3ead4b68 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductScreenRoute.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductScreenRoute.kt @@ -1,4 +1,5 @@ @file:Suppress("PackageNaming") + package com.hoc081098.solivagant.sample.search_products import androidx.compose.runtime.Immutable diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductsState.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductsState.kt index d2fa6bd7..fb07f0ed 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductsState.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductsState.kt @@ -1,4 +1,5 @@ @file:Suppress("PackageNaming") + package com.hoc081098.solivagant.sample.search_products import androidx.compose.runtime.Immutable From 1960c1ad9d312529c05cb9112c5e538e42fe5fb1 Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sat, 20 Jan 2024 16:24:22 +0700 Subject: [PATCH 10/28] format --- .../solivagant/sample/SolivagantSampleApp.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt index 86022c50..7b3c264a 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt @@ -15,6 +15,8 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -25,6 +27,7 @@ import coil3.annotation.ExperimentalCoilApi import coil3.compose.setSingletonImageLoaderFactory import coil3.network.NetworkFetcher import com.hoc081098.solivagant.navigation.BaseRoute +import com.hoc081098.solivagant.navigation.LocalLifecycleOwner import com.hoc081098.solivagant.navigation.NavEventNavigator import com.hoc081098.solivagant.navigation.NavHost import com.hoc081098.solivagant.navigation.NavRoot @@ -58,6 +61,25 @@ fun SolivagantSampleApp( .build() } + LocalLifecycleOwner.current.let { owner -> + LaunchedEffect(owner) { + owner.lifecycle.currentStateFlow.collect { + println("🚀🚀🚀 Lifecycle state changed: $it") + } + } + + DisposableEffect(owner) { + val cancellable = owner.lifecycle.subscribe { event -> + println("🚀🚀🚀 Lifecycle event: $event") + } + + onDispose { + cancellable.cancel() + println("🚀🚀🚀 Lifecycle state disposed") + } + } + } + var currentRoute: BaseRoute? by remember { mutableStateOf(null) } val destinations = remember { persistentSetOf( From f5065eaf99ecf0c6f26dae9120edd064e9b67ed0 Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sat, 20 Jan 2024 16:36:32 +0700 Subject: [PATCH 11/28] format --- .../navigation/LocalLifecycleOwner.kt | 2 +- .../solivagant/sample/SolivagantSampleApp.kt | 19 ---------------- .../solivagant/sample/start/StartScreen.kt | 22 +++++++++++++++++++ 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt index bac313dc..6616d348 100644 --- a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt +++ b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt @@ -32,7 +32,7 @@ public object LocalLifecycleOwner { get() = LocalLifecycleOwner.current /** - * Associates a [lifecycleOwnerProvidableCompositionLocal] key to a value in a call to + * Associates a [LocalLifecycleOwner] key to a value in a call to * [CompositionLocalProvider]. */ public infix fun provides(lifecycleOwner: LifecycleOwner): ProvidedValue = diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt index 7b3c264a..8851225c 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt @@ -61,25 +61,6 @@ fun SolivagantSampleApp( .build() } - LocalLifecycleOwner.current.let { owner -> - LaunchedEffect(owner) { - owner.lifecycle.currentStateFlow.collect { - println("🚀🚀🚀 Lifecycle state changed: $it") - } - } - - DisposableEffect(owner) { - val cancellable = owner.lifecycle.subscribe { event -> - println("🚀🚀🚀 Lifecycle event: $event") - } - - onDispose { - cancellable.cancel() - println("🚀🚀🚀 Lifecycle state disposed") - } - } - } - var currentRoute: BaseRoute? by remember { mutableStateOf(null) } val destinations = remember { persistentSetOf( diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/start/StartScreen.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/start/StartScreen.kt index e029a30a..faa1504e 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/start/StartScreen.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/start/StartScreen.kt @@ -8,17 +8,39 @@ import androidx.compose.foundation.layout.height import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.hoc081098.kmp.viewmodel.koin.compose.koinKmpViewModel +import com.hoc081098.solivagant.navigation.LocalLifecycleOwner @Composable internal fun StartScreen( modifier: Modifier = Modifier, viewModel: StartViewModel = koinKmpViewModel(), ) { + LocalLifecycleOwner.current.let { owner -> + LaunchedEffect(owner) { + owner.lifecycle.currentStateFlow.collect { + println("🚀🚀🚀 Lifecycle state changed: $it") + } + } + + DisposableEffect(owner) { + val cancellable = owner.lifecycle.subscribe { event -> + println("🚀🚀🚀 Lifecycle event: $event") + } + + onDispose { + cancellable.cancel() + println("🚀🚀🚀 Lifecycle state disposed") + } + } + } + Column( modifier = modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, From 65e3d6d624c5224849ea66469fd201519a2def7e Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sat, 20 Jan 2024 21:05:14 +0700 Subject: [PATCH 12/28] format --- lifecycle/api/android/lifecycle.api | 2 ++ lifecycle/api/jvm/lifecycle.api | 2 ++ .../solivagant/lifecycle/Lifecycle.kt | 26 ++++++++++++++++++- .../lifecycle/LifecycleOwnerRegistry.kt | 7 +---- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/lifecycle/api/android/lifecycle.api b/lifecycle/api/android/lifecycle.api index 7a02b0f4..6ae481bb 100644 --- a/lifecycle/api/android/lifecycle.api +++ b/lifecycle/api/android/lifecycle.api @@ -30,6 +30,7 @@ public final class com/hoc081098/solivagant/lifecycle/Lifecycle$Event : java/lan public static final fun downFrom (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; public static final fun downTo (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getTargetState ()Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; public static final fun upFrom (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; public static final fun upTo (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; public static fun valueOf (Ljava/lang/String;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; @@ -54,6 +55,7 @@ public final class com/hoc081098/solivagant/lifecycle/Lifecycle$State : java/lan public static final field RESUMED Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; public static final field STARTED Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun isAtLeast (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Z public static fun valueOf (Ljava/lang/String;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; public static fun values ()[Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; } diff --git a/lifecycle/api/jvm/lifecycle.api b/lifecycle/api/jvm/lifecycle.api index 5ee5f33a..1a47d1fc 100644 --- a/lifecycle/api/jvm/lifecycle.api +++ b/lifecycle/api/jvm/lifecycle.api @@ -23,6 +23,7 @@ public final class com/hoc081098/solivagant/lifecycle/Lifecycle$Event : java/lan public static final fun downFrom (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; public static final fun downTo (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getTargetState ()Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; public static final fun upFrom (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; public static final fun upTo (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; public static fun valueOf (Ljava/lang/String;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event; @@ -47,6 +48,7 @@ public final class com/hoc081098/solivagant/lifecycle/Lifecycle$State : java/lan public static final field RESUMED Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; public static final field STARTED Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun isAtLeast (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Z public static fun valueOf (Ljava/lang/String;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; public static fun values ()[Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; } diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/Lifecycle.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/Lifecycle.kt index aec152b5..63fa518f 100644 --- a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/Lifecycle.kt +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/Lifecycle.kt @@ -39,7 +39,15 @@ public interface Lifecycle { INITIALIZED, CREATED, STARTED, - RESUMED, + RESUMED; + + /** + * Compares if this State is greater or equal to the given `state`. + * + * @param state State to compare with + * @return true if this State is greater or equal to the given `state` + */ + public fun isAtLeast(state: State): Boolean = compareTo(state) >= 0 } public enum class Event { @@ -51,6 +59,22 @@ public interface Lifecycle { ON_DESTROY, ; + /** + * Returns the new [Lifecycle.State] of a [Lifecycle] that just reported + * this [Lifecycle.Event]. + * + * @return the state that will result from this event + */ + public val targetState: State + get() { + return when (this) { + ON_CREATE, ON_STOP -> State.CREATED + ON_START, ON_PAUSE -> State.STARTED + ON_RESUME -> State.RESUMED + ON_DESTROY -> State.DESTROYED + } + } + public companion object { /** * Returns the [Lifecycle.Event] that will be reported by a [Lifecycle] diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LifecycleOwnerRegistry.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LifecycleOwnerRegistry.kt index 3169bfe2..08bf128e 100644 --- a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LifecycleOwnerRegistry.kt +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LifecycleOwnerRegistry.kt @@ -30,12 +30,7 @@ public fun LifecycleRegistry( private class LifecycleRegistryImpl(initialState: State) : LifecycleRegistry { private val _currentStateFlow = MutableStateFlow(initialState) - - private inline var _state: State - get() = _currentStateFlow.value - set(value) { - _currentStateFlow.value = value - } + private var _state: State by _currentStateFlow::value private var observers: List = emptyList() From 5abe4807b4434a00af7278c96ba88734aa111cce Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sat, 20 Jan 2024 21:10:43 +0700 Subject: [PATCH 13/28] move LocalLifecycleOwner to lifecycle module --- .../api/android/khonshu-navigation-core.api | 8 -------- .../api/jvm/khonshu-navigation-core.api | 8 -------- .../hoc081098/solivagant/navigation/NavigationSetup.kt | 1 + lifecycle/api/android/lifecycle.api | 8 ++++++++ lifecycle/api/jvm/lifecycle.api | 8 ++++++++ lifecycle/build.gradle.kts | 7 +++++++ .../solivagant/lifecycle}/LocalLifecycleOwner.kt | 5 +---- .../kotlin/com/hoc081098/solivagant/navigation/NavHost.kt | 1 + .../hoc081098/solivagant/sample/SolivagantSampleApp.kt | 3 --- .../com/hoc081098/solivagant/sample/start/StartScreen.kt | 2 +- 10 files changed, 27 insertions(+), 24 deletions(-) rename {khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation => lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle}/LocalLifecycleOwner.kt (87%) diff --git a/khonshu-navigation-core/api/android/khonshu-navigation-core.api b/khonshu-navigation-core/api/android/khonshu-navigation-core.api index 773eb007..07b4840b 100644 --- a/khonshu-navigation-core/api/android/khonshu-navigation-core.api +++ b/khonshu-navigation-core/api/android/khonshu-navigation-core.api @@ -24,14 +24,6 @@ public final class com/hoc081098/solivagant/navigation/InitialValue$Creator : an public synthetic fun newArray (I)[Ljava/lang/Object; } -public final class com/hoc081098/solivagant/navigation/LocalLifecycleOwner { - public static final field $stable I - public static final field INSTANCE Lcom/hoc081098/solivagant/navigation/LocalLifecycleOwner; - public final fun getCurrent (Landroidx/compose/runtime/Composer;I)Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner; - public final fun provides (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;)Landroidx/compose/runtime/ProvidedValue; - public final fun providesDefault (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;)Landroidx/compose/runtime/ProvidedValue; -} - public abstract interface class com/hoc081098/solivagant/navigation/NavDestination { } diff --git a/khonshu-navigation-core/api/jvm/khonshu-navigation-core.api b/khonshu-navigation-core/api/jvm/khonshu-navigation-core.api index 0b3c1d62..0c7010b3 100644 --- a/khonshu-navigation-core/api/jvm/khonshu-navigation-core.api +++ b/khonshu-navigation-core/api/jvm/khonshu-navigation-core.api @@ -16,14 +16,6 @@ public final class com/hoc081098/solivagant/navigation/EXTRA_ROUTEKt { public static final field EXTRA_ROUTE Ljava/lang/String; } -public final class com/hoc081098/solivagant/navigation/LocalLifecycleOwner { - public static final field $stable I - public static final field INSTANCE Lcom/hoc081098/solivagant/navigation/LocalLifecycleOwner; - public final fun getCurrent (Landroidx/compose/runtime/Composer;I)Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner; - public final fun provides (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;)Landroidx/compose/runtime/ProvidedValue; - public final fun providesDefault (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;)Landroidx/compose/runtime/ProvidedValue; -} - public abstract interface class com/hoc081098/solivagant/navigation/NavDestination { } diff --git a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavigationSetup.kt b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavigationSetup.kt index 68ff472a..f33b40be 100644 --- a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavigationSetup.kt +++ b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavigationSetup.kt @@ -9,6 +9,7 @@ import com.hoc081098.kmp.viewmodel.parcelable.Parcelable import com.hoc081098.kmp.viewmodel.parcelable.Parcelize import com.hoc081098.solivagant.lifecycle.Lifecycle import com.hoc081098.solivagant.lifecycle.LifecycleOwner +import com.hoc081098.solivagant.lifecycle.LocalLifecycleOwner import com.hoc081098.solivagant.lifecycle.repeatOnLifecycle import com.hoc081098.solivagant.navigation.internal.InternalNavigationApi import com.hoc081098.solivagant.navigation.internal.NavEvent diff --git a/lifecycle/api/android/lifecycle.api b/lifecycle/api/android/lifecycle.api index 6ae481bb..8fab2ae8 100644 --- a/lifecycle/api/android/lifecycle.api +++ b/lifecycle/api/android/lifecycle.api @@ -72,6 +72,14 @@ public final class com/hoc081098/solivagant/lifecycle/LifecycleOwnerRegistryKt { public abstract interface class com/hoc081098/solivagant/lifecycle/LifecycleRegistry : com/hoc081098/solivagant/lifecycle/Lifecycle, com/hoc081098/solivagant/lifecycle/Lifecycle$Observer { } +public final class com/hoc081098/solivagant/lifecycle/LocalLifecycleOwner { + public static final field $stable I + public static final field INSTANCE Lcom/hoc081098/solivagant/lifecycle/LocalLifecycleOwner; + public final fun getCurrent (Landroidx/compose/runtime/Composer;I)Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner; + public final fun provides (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;)Landroidx/compose/runtime/ProvidedValue; + public final fun providesDefault (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;)Landroidx/compose/runtime/ProvidedValue; +} + public final class com/hoc081098/solivagant/lifecycle/RepeatOnLifecycleKt { public static final fun repeatOnLifecycle (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun repeatOnLifecycle (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/lifecycle/api/jvm/lifecycle.api b/lifecycle/api/jvm/lifecycle.api index 1a47d1fc..56c67ed0 100644 --- a/lifecycle/api/jvm/lifecycle.api +++ b/lifecycle/api/jvm/lifecycle.api @@ -65,6 +65,14 @@ public final class com/hoc081098/solivagant/lifecycle/LifecycleOwnerRegistryKt { public abstract interface class com/hoc081098/solivagant/lifecycle/LifecycleRegistry : com/hoc081098/solivagant/lifecycle/Lifecycle, com/hoc081098/solivagant/lifecycle/Lifecycle$Observer { } +public final class com/hoc081098/solivagant/lifecycle/LocalLifecycleOwner { + public static final field $stable I + public static final field INSTANCE Lcom/hoc081098/solivagant/lifecycle/LocalLifecycleOwner; + public final fun getCurrent (Landroidx/compose/runtime/Composer;I)Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner; + public final fun provides (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;)Landroidx/compose/runtime/ProvidedValue; + public final fun providesDefault (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;)Landroidx/compose/runtime/ProvidedValue; +} + public final class com/hoc081098/solivagant/lifecycle/RepeatOnLifecycleKt { public static final fun repeatOnLifecycle (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun repeatOnLifecycle (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/lifecycle/build.gradle.kts b/lifecycle/build.gradle.kts index 61104cb4..137388d0 100644 --- a/lifecycle/build.gradle.kts +++ b/lifecycle/build.gradle.kts @@ -6,6 +6,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget @Suppress("DSL_SCOPE_VIOLATION") plugins { alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.jetbrains.compose) alias(libs.plugins.android.library) alias(libs.plugins.poko) @@ -15,6 +16,10 @@ plugins { alias(libs.plugins.kotlinx.kover) } +compose { + kotlinCompilerPlugin.set(libs.versions.jetbrains.compose.compiler) +} + kotlin { explicitApi() @@ -64,6 +69,8 @@ kotlin { sourceSets { commonMain { dependencies { + api(compose.runtime) + api(libs.coroutines.core) } } diff --git a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LocalLifecycleOwner.kt similarity index 87% rename from khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt rename to lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LocalLifecycleOwner.kt index 6616d348..bef62efe 100644 --- a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/LocalLifecycleOwner.kt +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LocalLifecycleOwner.kt @@ -1,4 +1,4 @@ -package com.hoc081098.solivagant.navigation +package com.hoc081098.solivagant.lifecycle import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -6,8 +6,6 @@ import androidx.compose.runtime.ProvidedValue import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.Stable import androidx.compose.runtime.staticCompositionLocalOf -import com.hoc081098.solivagant.lifecycle.LifecycleOwner -import com.hoc081098.solivagant.navigation.internal.InternalNavigationApi /** * The CompositionLocal containing the current [LifecycleOwner]. @@ -38,7 +36,6 @@ public object LocalLifecycleOwner { public infix fun provides(lifecycleOwner: LifecycleOwner): ProvidedValue = LocalLifecycleOwner.provides(lifecycleOwner) - @InternalNavigationApi public infix fun providesDefault(lifecycleOwner: LifecycleOwner): ProvidedValue = LocalLifecycleOwner.providesDefault(lifecycleOwner) } diff --git a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt index d08b5c36..97a8a913 100644 --- a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt +++ b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier import com.hoc081098.kmp.viewmodel.Closeable import com.hoc081098.kmp.viewmodel.compose.SavedStateHandleFactoryProvider import com.hoc081098.kmp.viewmodel.compose.ViewModelStoreOwnerProvider +import com.hoc081098.solivagant.lifecycle.LocalLifecycleOwner import com.hoc081098.solivagant.navigation.internal.MultiStackNavigationExecutor import com.hoc081098.solivagant.navigation.internal.OnBackPressedCallback import com.hoc081098.solivagant.navigation.internal.StackEntry diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt index 8851225c..86022c50 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt @@ -15,8 +15,6 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -27,7 +25,6 @@ import coil3.annotation.ExperimentalCoilApi import coil3.compose.setSingletonImageLoaderFactory import coil3.network.NetworkFetcher import com.hoc081098.solivagant.navigation.BaseRoute -import com.hoc081098.solivagant.navigation.LocalLifecycleOwner import com.hoc081098.solivagant.navigation.NavEventNavigator import com.hoc081098.solivagant.navigation.NavHost import com.hoc081098.solivagant.navigation.NavRoot diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/start/StartScreen.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/start/StartScreen.kt index faa1504e..54b9ee02 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/start/StartScreen.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/start/StartScreen.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.hoc081098.kmp.viewmodel.koin.compose.koinKmpViewModel -import com.hoc081098.solivagant.navigation.LocalLifecycleOwner +import com.hoc081098.solivagant.lifecycle.LocalLifecycleOwner @Composable internal fun StartScreen( From 0e305d215de137234dae91b93eac032c8dbe8e18 Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sun, 21 Jan 2024 16:00:08 +0700 Subject: [PATCH 14/28] flowWithLifecycle, WithLifecycleState --- .../internal/AtomicReference.commonJvm.kt | 7 + .../solivagant/lifecycle/Lifecycle.kt | 3 +- .../solivagant/lifecycle/flowWithLifecycle.kt | 79 +++++++ .../lifecycle/internal/AtomicReference.kt | 6 + .../lifecycle/withLifecycleState.kt | 196 ++++++++++++++++++ .../lifecycle/internal/AtomicReference.js.kt | 11 + .../internal/AtomicReference.native.kt | 11 + 7 files changed, 312 insertions(+), 1 deletion(-) create mode 100644 lifecycle/src/commonJvmMain/kotlin/com/hoc081098/solivagant/lifecycle/internal/AtomicReference.commonJvm.kt create mode 100644 lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/flowWithLifecycle.kt create mode 100644 lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/internal/AtomicReference.kt create mode 100644 lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/withLifecycleState.kt create mode 100644 lifecycle/src/jsMain/kotlin/com/hoc081098/solivagant/lifecycle/internal/AtomicReference.js.kt create mode 100644 lifecycle/src/nativeMain/kotlin/com/hoc081098/solivagant/lifecycle/internal/AtomicReference.native.kt diff --git a/lifecycle/src/commonJvmMain/kotlin/com/hoc081098/solivagant/lifecycle/internal/AtomicReference.commonJvm.kt b/lifecycle/src/commonJvmMain/kotlin/com/hoc081098/solivagant/lifecycle/internal/AtomicReference.commonJvm.kt new file mode 100644 index 00000000..b6444b24 --- /dev/null +++ b/lifecycle/src/commonJvmMain/kotlin/com/hoc081098/solivagant/lifecycle/internal/AtomicReference.commonJvm.kt @@ -0,0 +1,7 @@ +package com.hoc081098.solivagant.lifecycle.internal + +import java.util.concurrent.atomic.AtomicReference + +// TODO: https://youtrack.jetbrains.com/issue/KT-37316 +@Suppress("ACTUAL_WITHOUT_EXPECT") // internal expect is not matched with internal typealias to public type +internal actual typealias AtomicReference = AtomicReference diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/Lifecycle.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/Lifecycle.kt index 63fa518f..b2f11038 100644 --- a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/Lifecycle.kt +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/Lifecycle.kt @@ -39,7 +39,8 @@ public interface Lifecycle { INITIALIZED, CREATED, STARTED, - RESUMED; + RESUMED, + ; /** * Compares if this State is greater or equal to the given `state`. diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/flowWithLifecycle.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/flowWithLifecycle.kt new file mode 100644 index 00000000..c9b4763a --- /dev/null +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/flowWithLifecycle.kt @@ -0,0 +1,79 @@ +package com.hoc081098.solivagant.lifecycle + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.launch + +/** + * Flow operator that emits values from `this` upstream Flow when the [lifecycle] is + * at least at [minActiveState] state. The emissions will be stopped when the lifecycle state + * falls below [minActiveState] state. + * + * The flow will automatically start and cancel collecting from `this` upstream flow as the + * [lifecycle] moves in and out of the target state. + * + * If [this] upstream Flow completes emitting items, `flowWithLifecycle` will trigger the flow + * collection again when the [minActiveState] state is reached. + * + * This is NOT a terminal operator. This operator is usually followed by [collect], or + * [onEach] and [launchIn] to process the emitted values. + * + * Note: this operator creates a hot flow that only closes when the [lifecycle] is destroyed or + * the coroutine that collects from the flow is cancelled. + * + * ``` + * class MyActivity : AppCompatActivity() { + * override fun onCreate(savedInstanceState: Bundle?) { + * /* ... */ + * // Launches a coroutine that collects items from a flow when the Activity + * // is at least started. It will automatically cancel when the activity is stopped and + * // start collecting again whenever it's started again. + * lifecycleScope.launch { + * flow + * .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) + * .collect { + * // Consume flow emissions + * } + * } + * } + * } + * ``` + * + * `flowWithLifecycle` cancels the upstream Flow when [lifecycle] falls below + * [minActiveState] state. However, the downstream Flow will be active without receiving any + * emissions as long as the scope used to collect the Flow is active. As such, please take care + * when using this function in an operator chain, as the order of the operators matters. For + * example, `flow1.flowWithLifecycle(lifecycle).combine(flow2)` behaves differently than + * `flow1.combine(flow2).flowWithLifecycle(lifecycle)`. The former continues to combine both + * flows even when [lifecycle] falls below [minActiveState] state whereas the combination is + * cancelled in the latter case. + * + * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a + * parameter will throw an [IllegalArgumentException]. + * + * Tip: If multiple flows need to be collected using `flowWithLifecycle`, consider using + * the [Lifecycle.repeatOnLifecycle] API to collect from all of them using a different + * [launch] per flow instead. That's more efficient and consumes less resources as no hot flows + * are created. + * + * @param lifecycle The [Lifecycle] where the restarting collecting from `this` flow work will be + * kept alive. + * @param minActiveState [Lifecycle.State] in which the upstream flow gets collected. The + * collection will stop if the lifecycle falls below that state, and will restart if it's in that + * state again. + * @return [Flow] that only emits items from `this` upstream flow when the [lifecycle] is at + * least in the [minActiveState]. + */ +public fun Flow.flowWithLifecycle( + lifecycle: Lifecycle, + minActiveState: Lifecycle.State = Lifecycle.State.STARTED, +): Flow = callbackFlow { + lifecycle.repeatOnLifecycle(minActiveState) { + this@flowWithLifecycle.collect { + send(it) + } + } + close() +} diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/internal/AtomicReference.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/internal/AtomicReference.kt new file mode 100644 index 00000000..414bcf5c --- /dev/null +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/internal/AtomicReference.kt @@ -0,0 +1,6 @@ +package com.hoc081098.solivagant.lifecycle.internal + +internal expect class AtomicReference(value: T) { + fun set(value: T) + fun getAndSet(value: T): T +} diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/withLifecycleState.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/withLifecycleState.kt new file mode 100644 index 00000000..6e8cd29b --- /dev/null +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/withLifecycleState.kt @@ -0,0 +1,196 @@ +package com.hoc081098.solivagant.lifecycle + +import com.hoc081098.solivagant.lifecycle.Lifecycle.Companion.currentState +import com.hoc081098.solivagant.lifecycle.internal.AtomicReference +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.coroutineContext +import kotlin.coroutines.resumeWithException +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Runnable +import kotlinx.coroutines.suspendCancellableCoroutine + +/** + * A [CancellationException] that indicates that the [Lifecycle] associated with an operation + * reached the [Lifecycle.State.DESTROYED] state before the operation could complete. + */ +public class LifecycleDestroyedException : CancellationException(null) + +/** + * Run [block] with this [Lifecycle] in a [Lifecycle.State] of at least [state] and + * resume with the result. Throws the [CancellationException] [LifecycleDestroyedException] + * if the lifecycle has reached [Lifecycle.State.DESTROYED] by the time of the call or before + * [block] is able to run. + */ +public suspend inline fun Lifecycle.withStateAtLeast( + state: Lifecycle.State, + crossinline block: () -> R, +): R { + require(state >= Lifecycle.State.CREATED) { + "target state must be CREATED or greater, found $state" + } + + return withStateAtLeastUnchecked(state, block) +} + +/** + * Run [block] with this [Lifecycle] in a [Lifecycle.State] of at least [Lifecycle.State.CREATED] + * and resume with the result. Throws the [CancellationException] [LifecycleDestroyedException] + * if the lifecycle has reached [Lifecycle.State.DESTROYED] by the time of the call or before + * [block] is able to run. + */ +public suspend inline fun Lifecycle.withCreated( + crossinline block: () -> R, +): R = withStateAtLeastUnchecked( + state = Lifecycle.State.CREATED, + block = block, +) + +/** + * Run [block] with this [Lifecycle] in a [Lifecycle.State] of at least [Lifecycle.State.STARTED] + * and resume with the result. Throws the [CancellationException] [LifecycleDestroyedException] + * if the lifecycle has reached [Lifecycle.State.DESTROYED] by the time of the call or before + * [block] is able to run. + */ +public suspend inline fun Lifecycle.withStarted( + crossinline block: () -> R, +): R = withStateAtLeastUnchecked( + state = Lifecycle.State.STARTED, + block = block, +) + +/** + * Run [block] with this [Lifecycle] in a [Lifecycle.State] of at least [Lifecycle.State.RESUMED] + * and resume with the result. Throws the [CancellationException] [LifecycleDestroyedException] + * if the lifecycle has reached [Lifecycle.State.DESTROYED] by the time of the call or before + * [block] is able to run. + */ +public suspend inline fun Lifecycle.withResumed( + crossinline block: () -> R, +): R = withStateAtLeastUnchecked( + state = Lifecycle.State.RESUMED, + block = block, +) + +/** + * Run [block] with this [LifecycleOwner]'s [Lifecycle] in a [Lifecycle.State] of at least [state] + * and resume with the result. Throws the [CancellationException] [LifecycleDestroyedException] + * if the lifecycle has reached [Lifecycle.State.DESTROYED] by the time of the call or before + * [block] is able to run. + */ +public suspend inline fun LifecycleOwner.withStateAtLeast( + state: Lifecycle.State, + crossinline block: () -> R, +): R = lifecycle.withStateAtLeast( + state = state, + block = block, +) + +/** + * Run [block] with this [LifecycleOwner]'s [Lifecycle] in a [Lifecycle.State] of at least + * [Lifecycle.State.CREATED] and resume with the result. + * Throws the [CancellationException] [LifecycleDestroyedException] if the lifecycle has reached + * [Lifecycle.State.DESTROYED] by the time of the call or before [block] is able to run. + */ +public suspend inline fun LifecycleOwner.withCreated( + crossinline block: () -> R, +): R = lifecycle.withStateAtLeastUnchecked( + state = Lifecycle.State.CREATED, + block = block, +) + +/** + * Run [block] with this [LifecycleOwner]'s [Lifecycle] in a [Lifecycle.State] of at least + * [Lifecycle.State.STARTED] and resume with the result. + * Throws the [CancellationException] [LifecycleDestroyedException] if the lifecycle has reached + * [Lifecycle.State.DESTROYED] by the time of the call or before [block] is able to run. + */ +public suspend inline fun LifecycleOwner.withStarted( + crossinline block: () -> R, +): R = lifecycle.withStateAtLeastUnchecked( + state = Lifecycle.State.STARTED, + block = block, +) + +/** + * Run [block] with this [LifecycleOwner]'s [Lifecycle] in a [Lifecycle.State] of at least + * [Lifecycle.State.RESUMED] and resume with the result. + * Throws the [CancellationException] [LifecycleDestroyedException] if the lifecycle has reached + * [Lifecycle.State.DESTROYED] by the time of the call or before [block] is able to run. + */ +public suspend inline fun LifecycleOwner.withResumed( + crossinline block: () -> R, +): R = lifecycle.withStateAtLeastUnchecked( + state = Lifecycle.State.RESUMED, + block = block, +) + +/** + * The inlined check for whether dispatch is necessary to perform [Lifecycle.withStateAtLeast] + * operations that does not bounds-check [state]. Used internally when we know the target state + * is already in bounds. Runs [block] inline without allocating if possible. + */ +@PublishedApi +internal suspend inline fun Lifecycle.withStateAtLeastUnchecked( + state: Lifecycle.State, + crossinline block: () -> R, +): R { + // Fast path: if our lifecycle dispatcher doesn't require dispatch we can check + // the current lifecycle state and decide if we can run synchronously + val lifecycleDispatcher = Dispatchers.Main.immediate + val dispatchNeeded = lifecycleDispatcher.isDispatchNeeded(coroutineContext) + if (!dispatchNeeded) { + if (currentState == Lifecycle.State.DESTROYED) throw LifecycleDestroyedException() + if (currentState >= state) return block() + } + + return suspendWithStateAtLeastUnchecked(state, dispatchNeeded, lifecycleDispatcher) { + block() + } +} + +/** + * The "slow" code path for [Lifecycle.withStateAtLeast] operations that requires allocating + * and suspending, factored into a non-inlined function to avoid inflating code size at call sites + * or exposing too many implementation details as inlined code. + */ +@PublishedApi +internal suspend fun Lifecycle.suspendWithStateAtLeastUnchecked( + state: Lifecycle.State, + dispatchNeeded: Boolean, + lifecycleDispatcher: CoroutineDispatcher, + block: () -> R, +): R = suspendCancellableCoroutine { co -> + val removeObserver = AtomicReference(null) + + val observer = Lifecycle.Observer { event -> + if (event == Lifecycle.Event.upTo(state)) { + removeObserver.getAndSet(null)?.cancel() + co.resumeWith(runCatching(block)) + } else if (event == Lifecycle.Event.ON_DESTROY) { + removeObserver.getAndSet(null)?.cancel() + co.resumeWithException(LifecycleDestroyedException()) + } + } + + if (dispatchNeeded) { + lifecycleDispatcher.dispatch( + EmptyCoroutineContext, + Runnable { removeObserver.set(subscribe(observer)) }, + ) + } else { + subscribe(observer).also(removeObserver::set) + } + + co.invokeOnCancellation { + if (lifecycleDispatcher.isDispatchNeeded(EmptyCoroutineContext)) { + lifecycleDispatcher.dispatch( + EmptyCoroutineContext, + Runnable { removeObserver.getAndSet(null)?.cancel() }, + ) + } else { + removeObserver.getAndSet(null)?.cancel() + } + } +} diff --git a/lifecycle/src/jsMain/kotlin/com/hoc081098/solivagant/lifecycle/internal/AtomicReference.js.kt b/lifecycle/src/jsMain/kotlin/com/hoc081098/solivagant/lifecycle/internal/AtomicReference.js.kt new file mode 100644 index 00000000..9e94d0e7 --- /dev/null +++ b/lifecycle/src/jsMain/kotlin/com/hoc081098/solivagant/lifecycle/internal/AtomicReference.js.kt @@ -0,0 +1,11 @@ +package com.hoc081098.solivagant.lifecycle.internal + +internal actual class AtomicReference actual constructor(value: T) { + private var _value: T = value + + actual fun set(value: T) { + _value = value + } + + actual fun getAndSet(value: T): T = _value.also { _value = value } +} diff --git a/lifecycle/src/nativeMain/kotlin/com/hoc081098/solivagant/lifecycle/internal/AtomicReference.native.kt b/lifecycle/src/nativeMain/kotlin/com/hoc081098/solivagant/lifecycle/internal/AtomicReference.native.kt new file mode 100644 index 00000000..9ce243f4 --- /dev/null +++ b/lifecycle/src/nativeMain/kotlin/com/hoc081098/solivagant/lifecycle/internal/AtomicReference.native.kt @@ -0,0 +1,11 @@ +package com.hoc081098.solivagant.lifecycle.internal + +internal actual class AtomicReference actual constructor(value: T) { + private val delegate = kotlin.concurrent.AtomicReference(value) + + actual fun set(value: T) { + delegate.value = value + } + + actual fun getAndSet(value: T): T = delegate.getAndSet(value) +} From 1aeb290b6f1d87a03f05f2ef19d6ff92a6ffc7af Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sun, 21 Jan 2024 16:00:54 +0700 Subject: [PATCH 15/28] flowWithLifecycle, WithLifecycleState --- lifecycle/api/android/lifecycle.api | 23 +++++++++++++++++++++++ lifecycle/api/jvm/lifecycle.api | 23 +++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/lifecycle/api/android/lifecycle.api b/lifecycle/api/android/lifecycle.api index 8fab2ae8..12369479 100644 --- a/lifecycle/api/android/lifecycle.api +++ b/lifecycle/api/android/lifecycle.api @@ -5,6 +5,11 @@ public final class com/hoc081098/solivagant/lifecycle/AndroidKt { public static final fun asSolivagantState (Landroidx/lifecycle/Lifecycle$State;)Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; } +public final class com/hoc081098/solivagant/lifecycle/FlowWithLifecycleKt { + public static final fun flowWithLifecycle (Lkotlinx/coroutines/flow/Flow;Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lkotlinx/coroutines/flow/Flow; + public static synthetic fun flowWithLifecycle$default (Lkotlinx/coroutines/flow/Flow;Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; +} + public abstract interface class com/hoc081098/solivagant/lifecycle/Lifecycle { public static final field Companion Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Companion; public abstract fun getCurrentStateFlow ()Lkotlinx/coroutines/flow/StateFlow; @@ -60,6 +65,11 @@ public final class com/hoc081098/solivagant/lifecycle/Lifecycle$State : java/lan public static fun values ()[Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; } +public final class com/hoc081098/solivagant/lifecycle/LifecycleDestroyedException : java/util/concurrent/CancellationException { + public static final field $stable I + public fun ()V +} + public abstract interface class com/hoc081098/solivagant/lifecycle/LifecycleOwner { public abstract fun getLifecycle ()Lcom/hoc081098/solivagant/lifecycle/Lifecycle; } @@ -85,3 +95,16 @@ public final class com/hoc081098/solivagant/lifecycle/RepeatOnLifecycleKt { public static final fun repeatOnLifecycle (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class com/hoc081098/solivagant/lifecycle/WithLifecycleStateKt { + public static final fun suspendWithStateAtLeastUnchecked (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;ZLkotlinx/coroutines/CoroutineDispatcher;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun withCreated (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun withCreated (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun withResumed (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun withResumed (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun withStarted (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun withStarted (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun withStateAtLeast (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun withStateAtLeast (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun withStateAtLeastUnchecked (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + diff --git a/lifecycle/api/jvm/lifecycle.api b/lifecycle/api/jvm/lifecycle.api index 56c67ed0..8cbceeb6 100644 --- a/lifecycle/api/jvm/lifecycle.api +++ b/lifecycle/api/jvm/lifecycle.api @@ -1,3 +1,8 @@ +public final class com/hoc081098/solivagant/lifecycle/FlowWithLifecycleKt { + public static final fun flowWithLifecycle (Lkotlinx/coroutines/flow/Flow;Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;)Lkotlinx/coroutines/flow/Flow; + public static synthetic fun flowWithLifecycle$default (Lkotlinx/coroutines/flow/Flow;Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; +} + public abstract interface class com/hoc081098/solivagant/lifecycle/Lifecycle { public static final field Companion Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Companion; public abstract fun getCurrentStateFlow ()Lkotlinx/coroutines/flow/StateFlow; @@ -53,6 +58,11 @@ public final class com/hoc081098/solivagant/lifecycle/Lifecycle$State : java/lan public static fun values ()[Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State; } +public final class com/hoc081098/solivagant/lifecycle/LifecycleDestroyedException : java/util/concurrent/CancellationException { + public static final field $stable I + public fun ()V +} + public abstract interface class com/hoc081098/solivagant/lifecycle/LifecycleOwner { public abstract fun getLifecycle ()Lcom/hoc081098/solivagant/lifecycle/Lifecycle; } @@ -78,3 +88,16 @@ public final class com/hoc081098/solivagant/lifecycle/RepeatOnLifecycleKt { public static final fun repeatOnLifecycle (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class com/hoc081098/solivagant/lifecycle/WithLifecycleStateKt { + public static final fun suspendWithStateAtLeastUnchecked (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;ZLkotlinx/coroutines/CoroutineDispatcher;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun withCreated (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun withCreated (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun withResumed (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun withResumed (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun withStarted (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun withStarted (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun withStateAtLeast (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun withStateAtLeast (Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun withStateAtLeastUnchecked (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + From 03e7439c25a7fd9e379cd7ebed627c514eee01d1 Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sun, 21 Jan 2024 16:01:53 +0700 Subject: [PATCH 16/28] flowWithLifecycle, WithLifecycleState --- .../solivagant/lifecycle/flowWithLifecycle.kt | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/flowWithLifecycle.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/flowWithLifecycle.kt index c9b4763a..0a23d386 100644 --- a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/flowWithLifecycle.kt +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/flowWithLifecycle.kt @@ -23,24 +23,6 @@ import kotlinx.coroutines.launch * Note: this operator creates a hot flow that only closes when the [lifecycle] is destroyed or * the coroutine that collects from the flow is cancelled. * - * ``` - * class MyActivity : AppCompatActivity() { - * override fun onCreate(savedInstanceState: Bundle?) { - * /* ... */ - * // Launches a coroutine that collects items from a flow when the Activity - * // is at least started. It will automatically cancel when the activity is stopped and - * // start collecting again whenever it's started again. - * lifecycleScope.launch { - * flow - * .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) - * .collect { - * // Consume flow emissions - * } - * } - * } - * } - * ``` - * * `flowWithLifecycle` cancels the upstream Flow when [lifecycle] falls below * [minActiveState] state. However, the downstream Flow will be active without receiving any * emissions as long as the scope used to collect the Flow is active. As such, please take care From 0e4c4118f01fceeda5f4d217352479cbedfc3a0f Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sun, 21 Jan 2024 16:12:33 +0700 Subject: [PATCH 17/28] flowWithLifecycle, WithLifecycleState --- .../android/StartActivity.kt | 19 +++++++++++++++++++ .../solivagant/sample/start/StartScreen.kt | 6 +++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/StartActivity.kt b/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/StartActivity.kt index da4fb085..92380d38 100644 --- a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/StartActivity.kt +++ b/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/StartActivity.kt @@ -3,9 +3,14 @@ package com.hoc081098.kmpviewmodelsample.android import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner import com.hoc081098.solivagant.sample.SolivagantSampleApp class StartActivity : ComponentActivity() { + private var addedObserver = false + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -13,4 +18,18 @@ class StartActivity : ComponentActivity() { SolivagantSampleApp() } } + + override fun onResume() { + super.onResume() + + if (!addedObserver) { + addedObserver = true + + lifecycle.addObserver(object : LifecycleEventObserver { + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + println("🚀🚀🚀 onStateChanged $event {${lifecycle.currentState}}") + } + }) + } + } } diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/start/StartScreen.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/start/StartScreen.kt index 54b9ee02..d6f18d9a 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/start/StartScreen.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/start/StartScreen.kt @@ -25,18 +25,18 @@ internal fun StartScreen( LocalLifecycleOwner.current.let { owner -> LaunchedEffect(owner) { owner.lifecycle.currentStateFlow.collect { - println("🚀🚀🚀 Lifecycle state changed: $it") + println("🚀🚀🚀 Lifecycle STATE: $it") } } DisposableEffect(owner) { val cancellable = owner.lifecycle.subscribe { event -> - println("🚀🚀🚀 Lifecycle event: $event") + println("🚀🚀🚀 Lifecycle EVENT: $event") } onDispose { cancellable.cancel() - println("🚀🚀🚀 Lifecycle state disposed") + println("🚀🚀🚀 Lifecycle EVENT disposed") } } } From 9f7bb6af6192162fcb82e056dd3564359a58321a Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sun, 21 Jan 2024 16:14:34 +0700 Subject: [PATCH 18/28] collectAsStateWithLifecycle --- .../compose/collectAsStateWithLifecycle.kt | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/collectAsStateWithLifecycle.kt diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/collectAsStateWithLifecycle.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/collectAsStateWithLifecycle.kt new file mode 100644 index 00000000..305bb94f --- /dev/null +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/collectAsStateWithLifecycle.kt @@ -0,0 +1,157 @@ +package com.hoc081098.solivagant.lifecycle.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.produceState +import com.hoc081098.solivagant.lifecycle.Lifecycle +import com.hoc081098.solivagant.lifecycle.LifecycleOwner +import com.hoc081098.solivagant.lifecycle.LocalLifecycleOwner +import com.hoc081098.solivagant.lifecycle.repeatOnLifecycle +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.withContext + +/** + * Collects values from this [StateFlow] and represents its latest value via [State] in a + * lifecycle-aware manner. + * + * The [StateFlow.value] is used as an initial value. Every time there would be new value posted + * into the [StateFlow] the returned [State] will be updated causing recomposition of every + * [State.value] usage whenever the [lifecycleOwner]'s lifecycle is at least [minActiveState]. + * + * This [StateFlow] is collected every time the [lifecycleOwner]'s lifecycle reaches the + * [minActiveState] Lifecycle state. The collection stops when the [lifecycleOwner]'s lifecycle + * falls below [minActiveState]. + * + * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a + * parameter will throw an [IllegalArgumentException]. + * + * @param lifecycleOwner [LifecycleOwner] whose `lifecycle` is used to restart collecting `this` + * flow. + * @param minActiveState [Lifecycle.State] in which the upstream flow gets collected. The + * collection will stop if the lifecycle falls below that state, and will restart if it's in that + * state again. + * @param context [CoroutineContext] to use for collecting. + */ +@Composable +public fun StateFlow.collectAsStateWithLifecycle( + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + minActiveState: Lifecycle.State = Lifecycle.State.STARTED, + context: CoroutineContext = EmptyCoroutineContext, +): State = collectAsStateWithLifecycle( + initialValue = this.value, + lifecycle = lifecycleOwner.lifecycle, + minActiveState = minActiveState, + context = context, +) + +/** + * Collects values from this [StateFlow] and represents its latest value via [State] in a + * lifecycle-aware manner. + * + * The [StateFlow.value] is used as an initial value. Every time there would be new value posted + * into the [StateFlow] the returned [State] will be updated causing recomposition of every + * [State.value] usage whenever the [lifecycle] is at least [minActiveState]. + * + * This [StateFlow] is collected every time [lifecycle] reaches the [minActiveState] Lifecycle + * state. The collection stops when [lifecycle] falls below [minActiveState]. + * + * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a + * parameter will throw an [IllegalArgumentException]. + * + * @param lifecycle [Lifecycle] used to restart collecting `this` flow. + * @param minActiveState [Lifecycle.State] in which the upstream flow gets collected. The + * collection will stop if the lifecycle falls below that state, and will restart if it's in that + * state again. + * @param context [CoroutineContext] to use for collecting. + */ +@Composable +public fun StateFlow.collectAsStateWithLifecycle( + lifecycle: Lifecycle, + minActiveState: Lifecycle.State = Lifecycle.State.STARTED, + context: CoroutineContext = EmptyCoroutineContext, +): State = collectAsStateWithLifecycle( + initialValue = this.value, + lifecycle = lifecycle, + minActiveState = minActiveState, + context = context, +) + +/** + * Collects values from this [Flow] and represents its latest value via [State] in a + * lifecycle-aware manner. + * + * Every time there would be new value posted into the [Flow] the returned [State] will be updated + * causing recomposition of every [State.value] usage whenever the [lifecycleOwner]'s lifecycle is + * at least [minActiveState]. + * + * This [Flow] is collected every time the [lifecycleOwner]'s lifecycle reaches the [minActiveState] + * Lifecycle state. The collection stops when the [lifecycleOwner]'s lifecycle falls below + * [minActiveState]. + * + * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a + * parameter will throw an [IllegalArgumentException]. + * + * @param initialValue The initial value given to the returned [State.value]. + * @param lifecycleOwner [LifecycleOwner] whose `lifecycle` is used to restart collecting `this` + * flow. + * @param minActiveState [Lifecycle.State] in which the upstream flow gets collected. The + * collection will stop if the lifecycle falls below that state, and will restart if it's in that + * state again. + * @param context [CoroutineContext] to use for collecting. + */ +@Composable +public fun Flow.collectAsStateWithLifecycle( + initialValue: T, + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + minActiveState: Lifecycle.State = Lifecycle.State.STARTED, + context: CoroutineContext = EmptyCoroutineContext, +): State = collectAsStateWithLifecycle( + initialValue = initialValue, + lifecycle = lifecycleOwner.lifecycle, + minActiveState = minActiveState, + context = context, +) + +/** + * Collects values from this [Flow] and represents its latest value via [State] in a + * lifecycle-aware manner. + * + * Every time there would be new value posted into the [Flow] the returned [State] will be updated + * causing recomposition of every [State.value] usage whenever the [lifecycle] is at + * least [minActiveState]. + * + * This [Flow] is collected every time [lifecycle] reaches the [minActiveState] Lifecycle + * state. The collection stops when [lifecycle] falls below [minActiveState]. + * + * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a + * parameter will throw an [IllegalArgumentException]. + * + * @param initialValue The initial value given to the returned [State.value]. + * @param lifecycle [Lifecycle] used to restart collecting `this` flow. + * @param minActiveState [Lifecycle.State] in which the upstream flow gets collected. The + * collection will stop if the lifecycle falls below that state, and will restart if it's in that + * state again. + * @param context [CoroutineContext] to use for collecting. + */ +@Composable +public fun Flow.collectAsStateWithLifecycle( + initialValue: T, + lifecycle: Lifecycle, + minActiveState: Lifecycle.State = Lifecycle.State.STARTED, + context: CoroutineContext = EmptyCoroutineContext, +): State { + return produceState(initialValue, this, lifecycle, minActiveState, context) { + lifecycle.repeatOnLifecycle(minActiveState) { + if (context == EmptyCoroutineContext) { + this@collectAsStateWithLifecycle.collect { this@produceState.value = it } + } else { + withContext(context) { + this@collectAsStateWithLifecycle.collect { this@produceState.value = it } + } + } + } + } +} From 2a9dbb22f64502c5844bff7a42565cb30ffe23dc Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sun, 21 Jan 2024 16:21:18 +0700 Subject: [PATCH 19/28] compose + lifecycle --- lifecycle/api/android/lifecycle.api | 45 ++ lifecycle/api/jvm/lifecycle.api | 45 ++ .../lifecycle/compose/LifecycleEffect.kt | 681 ++++++++++++++++++ .../lifecycle/compose/currentStateAsState.kt | 16 + 4 files changed, 787 insertions(+) create mode 100644 lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/LifecycleEffect.kt create mode 100644 lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/currentStateAsState.kt diff --git a/lifecycle/api/android/lifecycle.api b/lifecycle/api/android/lifecycle.api index 12369479..e417d265 100644 --- a/lifecycle/api/android/lifecycle.api +++ b/lifecycle/api/android/lifecycle.api @@ -108,3 +108,48 @@ public final class com/hoc081098/solivagant/lifecycle/WithLifecycleStateKt { public static final fun withStateAtLeastUnchecked (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class com/hoc081098/solivagant/lifecycle/compose/CollectAsStateWithLifecycleKt { + public static final fun collectAsStateWithLifecycle (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/State; + public static final fun collectAsStateWithLifecycle (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/State; + public static final fun collectAsStateWithLifecycle (Lkotlinx/coroutines/flow/StateFlow;Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/State; + public static final fun collectAsStateWithLifecycle (Lkotlinx/coroutines/flow/StateFlow;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/State; +} + +public final class com/hoc081098/solivagant/lifecycle/compose/CurrentStateAsStateKt { + public static final fun currentStateAsState (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Landroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/State; +} + +public final class com/hoc081098/solivagant/lifecycle/compose/LifecycleEffectKt { + public static final fun LifecycleEventEffect (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V + public static final fun LifecycleResumeEffect (Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun LifecycleResumeEffect (Ljava/lang/Object;Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun LifecycleResumeEffect (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun LifecycleResumeEffect ([Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun LifecycleStartEffect (Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun LifecycleStartEffect (Ljava/lang/Object;Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun LifecycleStartEffect (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun LifecycleStartEffect ([Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V +} + +public abstract interface class com/hoc081098/solivagant/lifecycle/compose/LifecyclePauseOrDisposeEffectResult { + public abstract fun runPauseOrOnDisposeEffect ()V +} + +public final class com/hoc081098/solivagant/lifecycle/compose/LifecycleResumePauseEffectScope : com/hoc081098/solivagant/lifecycle/LifecycleOwner { + public static final field $stable I + public fun (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;)V + public fun getLifecycle ()Lcom/hoc081098/solivagant/lifecycle/Lifecycle; + public final fun onPauseOrDispose (Lkotlin/jvm/functions/Function1;)Lcom/hoc081098/solivagant/lifecycle/compose/LifecyclePauseOrDisposeEffectResult; +} + +public final class com/hoc081098/solivagant/lifecycle/compose/LifecycleStartStopEffectScope : com/hoc081098/solivagant/lifecycle/LifecycleOwner { + public static final field $stable I + public fun (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;)V + public fun getLifecycle ()Lcom/hoc081098/solivagant/lifecycle/Lifecycle; + public final fun onStopOrDispose (Lkotlin/jvm/functions/Function1;)Lcom/hoc081098/solivagant/lifecycle/compose/LifecycleStopOrDisposeEffectResult; +} + +public abstract interface class com/hoc081098/solivagant/lifecycle/compose/LifecycleStopOrDisposeEffectResult { + public abstract fun runStopOrDisposeEffect ()V +} + diff --git a/lifecycle/api/jvm/lifecycle.api b/lifecycle/api/jvm/lifecycle.api index 8cbceeb6..f8eaa423 100644 --- a/lifecycle/api/jvm/lifecycle.api +++ b/lifecycle/api/jvm/lifecycle.api @@ -101,3 +101,48 @@ public final class com/hoc081098/solivagant/lifecycle/WithLifecycleStateKt { public static final fun withStateAtLeastUnchecked (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class com/hoc081098/solivagant/lifecycle/compose/CollectAsStateWithLifecycleKt { + public static final fun collectAsStateWithLifecycle (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/State; + public static final fun collectAsStateWithLifecycle (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/State; + public static final fun collectAsStateWithLifecycle (Lkotlinx/coroutines/flow/StateFlow;Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/State; + public static final fun collectAsStateWithLifecycle (Lkotlinx/coroutines/flow/StateFlow;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lcom/hoc081098/solivagant/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/State; +} + +public final class com/hoc081098/solivagant/lifecycle/compose/CurrentStateAsStateKt { + public static final fun currentStateAsState (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Landroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/State; +} + +public final class com/hoc081098/solivagant/lifecycle/compose/LifecycleEffectKt { + public static final fun LifecycleEventEffect (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V + public static final fun LifecycleResumeEffect (Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun LifecycleResumeEffect (Ljava/lang/Object;Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun LifecycleResumeEffect (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun LifecycleResumeEffect ([Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun LifecycleStartEffect (Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun LifecycleStartEffect (Ljava/lang/Object;Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun LifecycleStartEffect (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun LifecycleStartEffect ([Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V +} + +public abstract interface class com/hoc081098/solivagant/lifecycle/compose/LifecyclePauseOrDisposeEffectResult { + public abstract fun runPauseOrOnDisposeEffect ()V +} + +public final class com/hoc081098/solivagant/lifecycle/compose/LifecycleResumePauseEffectScope : com/hoc081098/solivagant/lifecycle/LifecycleOwner { + public static final field $stable I + public fun (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;)V + public fun getLifecycle ()Lcom/hoc081098/solivagant/lifecycle/Lifecycle; + public final fun onPauseOrDispose (Lkotlin/jvm/functions/Function1;)Lcom/hoc081098/solivagant/lifecycle/compose/LifecyclePauseOrDisposeEffectResult; +} + +public final class com/hoc081098/solivagant/lifecycle/compose/LifecycleStartStopEffectScope : com/hoc081098/solivagant/lifecycle/LifecycleOwner { + public static final field $stable I + public fun (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;)V + public fun getLifecycle ()Lcom/hoc081098/solivagant/lifecycle/Lifecycle; + public final fun onStopOrDispose (Lkotlin/jvm/functions/Function1;)Lcom/hoc081098/solivagant/lifecycle/compose/LifecycleStopOrDisposeEffectResult; +} + +public abstract interface class com/hoc081098/solivagant/lifecycle/compose/LifecycleStopOrDisposeEffectResult { + public abstract fun runStopOrDisposeEffect ()V +} + diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/LifecycleEffect.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/LifecycleEffect.kt new file mode 100644 index 00000000..cb004263 --- /dev/null +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/LifecycleEffect.kt @@ -0,0 +1,681 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hoc081098.solivagant.lifecycle.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import com.hoc081098.solivagant.lifecycle.Lifecycle +import com.hoc081098.solivagant.lifecycle.LifecycleOwner +import com.hoc081098.solivagant.lifecycle.LocalLifecycleOwner + +/** + * Schedule an effect to run when the [Lifecycle] receives a specific [Lifecycle.Event]. + * + * Using a [Lifecycle.Observer] to listen for when [LifecycleEventEffect] enters + * the composition, [onEvent] will be launched when receiving the specified [event]. + * + * This function should **not** be used to listen for [Lifecycle.Event.ON_DESTROY] because + * Compose stops recomposing after receiving a [Lifecycle.Event.ON_STOP] and will never be + * aware of an ON_DESTROY to launch [onEvent]. + * + * This function should also **not** be used to launch tasks in response to callback + * events by way of storing callback data as a [Lifecycle.State] in a [MutableState]. + * Instead, see [currentStateAsState] to obtain a [State][State] + * that may be used to launch jobs in response to state changes. + * + * @param event The [Lifecycle.Event] to listen for + * @param lifecycleOwner The lifecycle owner to attach an observer + * @param onEvent The effect to be launched when we receive an [event] callback + * + * @throws IllegalArgumentException if attempting to listen for [Lifecycle.Event.ON_DESTROY] + */ +@Composable +public fun LifecycleEventEffect( + event: Lifecycle.Event, + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + onEvent: () -> Unit +) { + if (event == Lifecycle.Event.ON_DESTROY) { + throw IllegalArgumentException( + "LifecycleEventEffect cannot be used to " + + "listen for Lifecycle.Event.ON_DESTROY, since Compose disposes of the " + + "composition before ON_DESTROY observers are invoked." + ) + } + + // Safely update the current `onEvent` lambda when a new one is provided + val currentOnEvent by rememberUpdatedState(onEvent) + DisposableEffect(lifecycleOwner) { + val cancellable = lifecycleOwner.lifecycle.subscribe { e -> + if (e == event) { + currentOnEvent() + } + } + onDispose(cancellable::cancel) + } +} + +/** + * Schedule a pair of effects to run when the [Lifecycle] receives either a + * [Lifecycle.Event.ON_START] or [Lifecycle.Event.ON_STOP] (or any new unique + * value of [key1]). The ON_START effect will be the body of the [effects] + * block and the ON_STOP effect will be within the + * (onStopOrDispose clause)[LifecycleStartStopEffectScope.onStopOrDispose]: + * + * ``` + * LifecycleStartEffect(lifecycleOwner) { + * // add ON_START effect here + * + * onStopOrDispose { + * // add clean up for work kicked off in the ON_START effect here + * } + * } + * ``` + * + * A [LifecycleStartEffect] **must** include an + * [onStopOrDispose][LifecycleStartStopEffectScope.onStopOrDispose] clause as the final + * statement in its [effects] block. If your operation does not require an effect for + * _both_ [Lifecycle.Event.ON_START] and [Lifecycle.Event.ON_STOP], a [LifecycleEventEffect] + * should be used instead. + * + * A [LifecycleStartEffect]'s _key_ is a value that defines the identity of the effect. + * If the key changes, the [LifecycleStartEffect] must + * [dispose][LifecycleStartStopEffectScope.onStopOrDispose] its current [effects] and + * reset by calling [effects] again. Examples of keys include: + * + * * Observable objects that the effect subscribes to + * * Unique request parameters to an operation that must cancel and retry if those parameters change + * + * This function uses a [Lifecycle.Observer] to listen for when [LifecycleStartEffect] + * enters the composition and the effects will be launched when receiving a + * [Lifecycle.Event.ON_START] or [Lifecycle.Event.ON_STOP] event, respectively. If the + * [LifecycleStartEffect] leaves the composition prior to receiving an [Lifecycle.Event.ON_STOP] + * event, [onStopOrDispose][LifecycleStartStopEffectScope.onStopOrDispose] will be called to + * clean up the work that was kicked off in the ON_START effect. + * + * This function should **not** be used to launch tasks in response to callback + * events by way of storing callback data as a [Lifecycle.State] in a [MutableState]. + * Instead, see [currentStateAsState] to obtain a [State][State] + * that may be used to launch jobs in response to state changes. + * + * @param key1 The unique value to trigger recomposition upon change + * @param lifecycleOwner The lifecycle owner to attach an observer + * @param effects The effects to be launched when we receive the respective event callbacks + */ +@Composable +public fun LifecycleStartEffect( + key1: Any?, + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + effects: LifecycleStartStopEffectScope.() -> LifecycleStopOrDisposeEffectResult +) { + val lifecycleStartStopEffectScope = remember(key1, lifecycleOwner) { + LifecycleStartStopEffectScope(lifecycleOwner.lifecycle) + } + LifecycleStartEffectImpl(lifecycleOwner, lifecycleStartStopEffectScope, effects) +} + +/** + * Schedule a pair of effects to run when the [Lifecycle] receives either a + * [Lifecycle.Event.ON_START] or [Lifecycle.Event.ON_STOP] (or any new unique + * value of [key1] or [key2]). The ON_START effect will be the body of the + * [effects] block and the ON_STOP effect will be within the + * (onStopOrDispose clause)[LifecycleStartStopEffectScope.onStopOrDispose]: + * + * ``` + * LifecycleStartEffect(lifecycleOwner) { + * // add ON_START effect here + * + * onStopOrDispose { + * // add clean up for work kicked off in the ON_START effect here + * } + * } + * ``` + * + * A [LifecycleStartEffect] **must** include an + * [onStopOrDispose][LifecycleStartStopEffectScope.onStopOrDispose] clause as the final + * statement in its [effects] block. If your operation does not require an effect for + * _both_ [Lifecycle.Event.ON_START] and [Lifecycle.Event.ON_STOP], a [LifecycleEventEffect] + * should be used instead. + * + * A [LifecycleStartEffect]'s _key_ is a value that defines the identity of the effect. + * If a key changes, the [LifecycleStartEffect] must + * [dispose][LifecycleStartStopEffectScope.onStopOrDispose] its current [effects] and + * reset by calling [effects] again. Examples of keys include: + * + * * Observable objects that the effect subscribes to + * * Unique request parameters to an operation that must cancel and retry if those parameters change + * + * This function uses a [Lifecycle.Observer] to listen for when [LifecycleStartEffect] + * enters the composition and the effects will be launched when receiving a + * [Lifecycle.Event.ON_START] or [Lifecycle.Event.ON_STOP] event, respectively. If the + * [LifecycleStartEffect] leaves the composition prior to receiving an [Lifecycle.Event.ON_STOP] + * event, [onStopOrDispose][LifecycleStartStopEffectScope.onStopOrDispose] will be called to + * clean up the work that was kicked off in the ON_START effect. + * + * This function should **not** be used to launch tasks in response to callback + * events by way of storing callback data as a [Lifecycle.State] in a [MutableState]. + * Instead, see [currentStateAsState] to obtain a [State][State] + * that may be used to launch jobs in response to state changes. + * + * @param key1 A unique value to trigger recomposition upon change + * @param key2 A unique value to trigger recomposition upon change + * @param lifecycleOwner The lifecycle owner to attach an observer + * @param effects The effects to be launched when we receive the respective event callbacks + */ +@Composable +public fun LifecycleStartEffect( + key1: Any?, + key2: Any?, + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + effects: LifecycleStartStopEffectScope.() -> LifecycleStopOrDisposeEffectResult +) { + val lifecycleStartStopEffectScope = remember(key1, key2, lifecycleOwner) { + LifecycleStartStopEffectScope(lifecycleOwner.lifecycle) + } + LifecycleStartEffectImpl(lifecycleOwner, lifecycleStartStopEffectScope, effects) +} + +/** + * Schedule a pair of effects to run when the [Lifecycle] receives either a + * [Lifecycle.Event.ON_START] or [Lifecycle.Event.ON_STOP] (or any new unique + * value of [key1] or [key2] or [key3]). The ON_START effect will be the body + * of the [effects] block and the ON_STOP effect will be within the + * (onStopOrDispose clause)[LifecycleStartStopEffectScope.onStopOrDispose]: + * + * ``` + * LifecycleStartEffect(lifecycleOwner) { + * // add ON_START effect here + * + * onStopOrDispose { + * // add clean up for work kicked off in the ON_START effect here + * } + * } + * ``` + * + * A [LifecycleStartEffect] **must** include an + * [onStopOrDispose][LifecycleStartStopEffectScope.onStopOrDispose] clause as the final + * statement in its [effects] block. If your operation does not require an effect for + * _both_ [Lifecycle.Event.ON_START] and [Lifecycle.Event.ON_STOP], a [LifecycleEventEffect] + * should be used instead. + * + * A [LifecycleStartEffect]'s _key_ is a value that defines the identity of the effect. + * If a key changes, the [LifecycleStartEffect] must + * [dispose][LifecycleStartStopEffectScope.onStopOrDispose] its current [effects] and + * reset by calling [effects] again. Examples of keys include: + * + * * Observable objects that the effect subscribes to + * * Unique request parameters to an operation that must cancel and retry if those parameters change + * + * This function uses a [Lifecycle.Observer] to listen for when [LifecycleStartEffect] + * enters the composition and the effects will be launched when receiving a + * [Lifecycle.Event.ON_START] or [Lifecycle.Event.ON_STOP] event, respectively. If the + * [LifecycleStartEffect] leaves the composition prior to receiving an [Lifecycle.Event.ON_STOP] + * event, [onStopOrDispose][LifecycleStartStopEffectScope.onStopOrDispose] will be called to + * clean up the work that was kicked off in the ON_START effect. + * + * This function should **not** be used to launch tasks in response to callback + * events by way of storing callback data as a [Lifecycle.State] in a [MutableState]. + * Instead, see [currentStateAsState] to obtain a [State][State] + * that may be used to launch jobs in response to state changes. + * + * @param key1 The unique value to trigger recomposition upon change + * @param key2 The unique value to trigger recomposition upon change + * @param key3 The unique value to trigger recomposition upon change + * @param lifecycleOwner The lifecycle owner to attach an observer + * @param effects The effects to be launched when we receive the respective event callbacks + */ +@Composable +public fun LifecycleStartEffect( + key1: Any?, + key2: Any?, + key3: Any?, + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + effects: LifecycleStartStopEffectScope.() -> LifecycleStopOrDisposeEffectResult +) { + val lifecycleStartStopEffectScope = remember(key1, key2, key3, lifecycleOwner) { + LifecycleStartStopEffectScope(lifecycleOwner.lifecycle) + } + LifecycleStartEffectImpl(lifecycleOwner, lifecycleStartStopEffectScope, effects) +} + +/** + * Schedule a pair of effects to run when the [Lifecycle] receives either a + * [Lifecycle.Event.ON_START] or [Lifecycle.Event.ON_STOP] (or any new unique + * value of [keys]). The ON_START effect will be the body of the [effects] + * block and the ON_STOP effect will be within the + * (onStopOrDispose clause)[LifecycleStartStopEffectScope.onStopOrDispose]: + * + * ``` + * LifecycleStartEffect(lifecycleOwner) { + * // add ON_START effect here + * + * onStopOrDispose { + * // add clean up for work kicked off in the ON_START effect here + * } + * } + * ``` + * + * A [LifecycleStartEffect] **must** include an + * [onStopOrDispose][LifecycleStartStopEffectScope.onStopOrDispose] clause as the final + * statement in its [effects] block. If your operation does not require an effect for + * _both_ [Lifecycle.Event.ON_START] and [Lifecycle.Event.ON_STOP], a [LifecycleEventEffect] + * should be used instead. + * + * A [LifecycleStartEffect]'s _key_ is a value that defines the identity of the effect. + * If a key changes, the [LifecycleStartEffect] must + * [dispose][LifecycleStartStopEffectScope.onStopOrDispose] its current [effects] and + * reset by calling [effects] again. Examples of keys include: + * + * * Observable objects that the effect subscribes to + * * Unique request parameters to an operation that must cancel and retry if those parameters change + * + * This function uses a [Lifecycle.Observer] to listen for when [LifecycleStartEffect] + * enters the composition and the effects will be launched when receiving a + * [Lifecycle.Event.ON_START] or [Lifecycle.Event.ON_STOP] event, respectively. If the + * [LifecycleStartEffect] leaves the composition prior to receiving an [Lifecycle.Event.ON_STOP] + * event, [onStopOrDispose][LifecycleStartStopEffectScope.onStopOrDispose] will be called to + * clean up the work that was kicked off in the ON_START effect. + * + * This function should **not** be used to launch tasks in response to callback + * events by way of storing callback data as a [Lifecycle.State] in a [MutableState]. + * Instead, see [currentStateAsState] to obtain a [State][State] + * that may be used to launch jobs in response to state changes. + * + * @param keys The unique values to trigger recomposition upon changes + * @param lifecycleOwner The lifecycle owner to attach an observer + * @param effects The effects to be launched when we receive the respective event callbacks + */ +@Composable +public fun LifecycleStartEffect( + vararg keys: Any?, + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + effects: LifecycleStartStopEffectScope.() -> LifecycleStopOrDisposeEffectResult +) { + val lifecycleStartStopEffectScope = remember(*keys, lifecycleOwner) { + LifecycleStartStopEffectScope(lifecycleOwner.lifecycle) + } + LifecycleStartEffectImpl(lifecycleOwner, lifecycleStartStopEffectScope, effects) +} + +@Composable +private fun LifecycleStartEffectImpl( + lifecycleOwner: LifecycleOwner, + scope: LifecycleStartStopEffectScope, + effects: LifecycleStartStopEffectScope.() -> LifecycleStopOrDisposeEffectResult +) { + DisposableEffect(lifecycleOwner, scope) { + var effectResult: LifecycleStopOrDisposeEffectResult? = null + + val cancellable = lifecycleOwner.lifecycle.subscribe { event -> + when (event) { + Lifecycle.Event.ON_START -> with(scope) { + effectResult = effects() + } + + Lifecycle.Event.ON_STOP -> effectResult?.runStopOrDisposeEffect() + + else -> {} + } + } + + onDispose { + cancellable.cancel() + effectResult?.runStopOrDisposeEffect() + } + } +} + +/** + * Interface used for [LifecycleStartEffect] to run the effect within the onStopOrDispose + * clause when an (ON_STOP)[Lifecycle.Event.ON_STOP] event is received or when cleanup is + * needed for the work that was kicked off in the ON_START effect. + */ +public interface LifecycleStopOrDisposeEffectResult { + public fun runStopOrDisposeEffect() +} + +/** + * Receiver scope for [LifecycleStartEffect] that offers the [onStopOrDispose] clause to + * couple the ON_START effect. This should be the last statement in any call to + * [LifecycleStartEffect]. + * + * This scope is also a [LifecycleOwner] to allow access to the + * (lifecycle)[LifecycleStartStopEffectScope.lifecycle] within the [onStopOrDispose] clause. + * + * @param lifecycle The lifecycle being observed by this receiver scope + */ +public class LifecycleStartStopEffectScope(override val lifecycle: Lifecycle) : LifecycleOwner { + /** + * Provide the [onStopOrDisposeEffect] to the [LifecycleStartEffect] to run when the + * observer receives an (ON_STOP)[Lifecycle.Event.ON_STOP] event or must undergo cleanup. + */ + public inline fun onStopOrDispose( + crossinline onStopOrDisposeEffect: LifecycleOwner.() -> Unit + ): LifecycleStopOrDisposeEffectResult = object : LifecycleStopOrDisposeEffectResult { + override fun runStopOrDisposeEffect() { + onStopOrDisposeEffect() + } + } +} + +/** + * Schedule a pair of effects to run when the [Lifecycle] receives either a + * [Lifecycle.Event.ON_RESUME] or [Lifecycle.Event.ON_PAUSE] (or any new unique + * value of [key1]). The ON_RESUME effect will be the body of the [effects] + * block and the ON_PAUSE effect will be within the + * (onPauseOrDispose clause)[LifecycleResumePauseEffectScope.onPauseOrDispose]: + * + * ``` + * LifecycleResumeEffect(lifecycleOwner) { + * // add ON_RESUME effect here + * + * onPauseOrDispose { + * // add clean up for work kicked off in the ON_RESUME effect here + * } + * } + * ``` + * + * A [LifecycleResumeEffect] **must** include an + * [onPauseOrDispose][LifecycleResumePauseEffectScope.onPauseOrDispose] clause as + * the final statement in its [effects] block. If your operation does not require + * an effect for _both_ [Lifecycle.Event.ON_RESUME] and [Lifecycle.Event.ON_PAUSE], + * a [LifecycleEventEffect] should be used instead. + * + * A [LifecycleResumeEffect]'s _key_ is a value that defines the identity of the effect. + * If a key changes, the [LifecycleResumeEffect] must + * [dispose][LifecycleResumePauseEffectScope.onPauseOrDispose] its current [effects] and + * reset by calling [effects] again. Examples of keys include: + * + * * Observable objects that the effect subscribes to + * * Unique request parameters to an operation that must cancel and retry if those parameters change + * + * This function uses a [Lifecycle.Observer] to listen for when [LifecycleResumeEffect] + * enters the composition and the effects will be launched when receiving a + * [Lifecycle.Event.ON_RESUME] or [Lifecycle.Event.ON_PAUSE] event, respectively. If the + * [LifecycleResumeEffect] leaves the composition prior to receiving an [Lifecycle.Event.ON_PAUSE] + * event, [onPauseOrDispose][LifecycleResumePauseEffectScope.onPauseOrDispose] will be called + * to clean up the work that was kicked off in the ON_RESUME effect. + * + * This function should **not** be used to launch tasks in response to callback + * events by way of storing callback data as a [Lifecycle.State] in a [MutableState]. + * Instead, see [currentStateAsState] to obtain a [State][State] + * that may be used to launch jobs in response to state changes. + * + * @param key1 The unique value to trigger recomposition upon change + * @param lifecycleOwner The lifecycle owner to attach an observer + * @param effects The effects to be launched when we receive the respective event callbacks + */ +@Composable +public fun LifecycleResumeEffect( + key1: Any?, + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + effects: LifecycleResumePauseEffectScope.() -> LifecyclePauseOrDisposeEffectResult +) { + val lifecycleResumePauseEffectScope = remember(key1, lifecycleOwner) { + LifecycleResumePauseEffectScope(lifecycleOwner.lifecycle) + } + LifecycleResumeEffectImpl(lifecycleOwner, lifecycleResumePauseEffectScope, effects) +} + +/** + * Schedule a pair of effects to run when the [Lifecycle] receives either a + * [Lifecycle.Event.ON_RESUME] or [Lifecycle.Event.ON_PAUSE] (or any new unique + * value of [key1] or [key2]). The ON_RESUME effect will be the body of the + * [effects] block and the ON_PAUSE effect will be within the + * (onPauseOrDispose clause)[LifecycleResumePauseEffectScope.onPauseOrDispose]: + * + * ``` + * LifecycleResumeEffect(lifecycleOwner) { + * // add ON_RESUME effect here + * + * onPauseOrDispose { + * // add clean up for work kicked off in the ON_RESUME effect here + * } + * } + * ``` + * + * A [LifecycleResumeEffect] **must** include an + * [onPauseOrDispose][LifecycleResumePauseEffectScope.onPauseOrDispose] clause as + * the final statement in its [effects] block. If your operation does not require + * an effect for _both_ [Lifecycle.Event.ON_RESUME] and [Lifecycle.Event.ON_PAUSE], + * a [LifecycleEventEffect] should be used instead. + * + * A [LifecycleResumeEffect]'s _key_ is a value that defines the identity of the effect. + * If a key changes, the [LifecycleResumeEffect] must + * [dispose][LifecycleResumePauseEffectScope.onPauseOrDispose] its current [effects] and + * reset by calling [effects] again. Examples of keys include: + * + * * Observable objects that the effect subscribes to + * * Unique request parameters to an operation that must cancel and retry if those parameters change + * + * This function uses a [Lifecycle.Observer] to listen for when [LifecycleResumeEffect] + * enters the composition and the effects will be launched when receiving a + * [Lifecycle.Event.ON_RESUME] or [Lifecycle.Event.ON_PAUSE] event, respectively. If the + * [LifecycleResumeEffect] leaves the composition prior to receiving an [Lifecycle.Event.ON_PAUSE] + * event, [onPauseOrDispose][LifecycleResumePauseEffectScope.onPauseOrDispose] will be called + * to clean up the work that was kicked off in the ON_RESUME effect. + * + * This function should **not** be used to launch tasks in response to callback + * events by way of storing callback data as a [Lifecycle.State] in a [MutableState]. + * Instead, see [currentStateAsState] to obtain a [State][State] + * that may be used to launch jobs in response to state changes. + * + * @param key1 A unique value to trigger recomposition upon change + * @param key2 A unique value to trigger recomposition upon change + * @param lifecycleOwner The lifecycle owner to attach an observer + * @param effects The effects to be launched when we receive the respective event callbacks + */ +@Composable +public fun LifecycleResumeEffect( + key1: Any?, + key2: Any?, + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + effects: LifecycleResumePauseEffectScope.() -> LifecyclePauseOrDisposeEffectResult +) { + val lifecycleResumePauseEffectScope = remember(key1, key2, lifecycleOwner) { + LifecycleResumePauseEffectScope(lifecycleOwner.lifecycle) + } + LifecycleResumeEffectImpl(lifecycleOwner, lifecycleResumePauseEffectScope, effects) +} + +/** + * Schedule a pair of effects to run when the [Lifecycle] receives either a + * [Lifecycle.Event.ON_RESUME] or [Lifecycle.Event.ON_PAUSE] (or any new unique + * value of [key1] or [key2] or [key3]). The ON_RESUME effect will be the body + * of the [effects] block and the ON_PAUSE effect will be within the + * (onPauseOrDispose clause)[LifecycleResumePauseEffectScope.onPauseOrDispose]: + * + * ``` + * LifecycleResumeEffect(lifecycleOwner) { + * // add ON_RESUME effect here + * + * onPauseOrDispose { + * // add clean up for work kicked off in the ON_RESUME effect here + * } + * } + * ``` + * + * A [LifecycleResumeEffect] **must** include an + * [onPauseOrDispose][LifecycleResumePauseEffectScope.onPauseOrDispose] clause as + * the final statement in its [effects] block. If your operation does not require + * an effect for _both_ [Lifecycle.Event.ON_RESUME] and [Lifecycle.Event.ON_PAUSE], + * a [LifecycleEventEffect] should be used instead. + * + * A [LifecycleResumeEffect]'s _key_ is a value that defines the identity of the effect. + * If a key changes, the [LifecycleResumeEffect] must + * [dispose][LifecycleResumePauseEffectScope.onPauseOrDispose] its current [effects] and + * reset by calling [effects] again. Examples of keys include: + * + * * Observable objects that the effect subscribes to + * * Unique request parameters to an operation that must cancel and retry if those parameters change + * + * This function uses a [Lifecycle.Observer] to listen for when [LifecycleResumeEffect] + * enters the composition and the effects will be launched when receiving a + * [Lifecycle.Event.ON_RESUME] or [Lifecycle.Event.ON_PAUSE] event, respectively. If the + * [LifecycleResumeEffect] leaves the composition prior to receiving an [Lifecycle.Event.ON_PAUSE] + * event, [onPauseOrDispose][LifecycleResumePauseEffectScope.onPauseOrDispose] will be called + * to clean up the work that was kicked off in the ON_RESUME effect. + * + * This function should **not** be used to launch tasks in response to callback + * events by way of storing callback data as a [Lifecycle.State] in a [MutableState]. + * Instead, see [currentStateAsState] to obtain a [State][State] + * that may be used to launch jobs in response to state changes. + * + * @param key1 A unique value to trigger recomposition upon change + * @param key2 A unique value to trigger recomposition upon change + * @param key3 A unique value to trigger recomposition upon change + * @param lifecycleOwner The lifecycle owner to attach an observer + * @param effects The effects to be launched when we receive the respective event callbacks + */ +@Composable +public fun LifecycleResumeEffect( + key1: Any?, + key2: Any?, + key3: Any?, + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + effects: LifecycleResumePauseEffectScope.() -> LifecyclePauseOrDisposeEffectResult +) { + val lifecycleResumePauseEffectScope = remember(key1, key2, key3, lifecycleOwner) { + LifecycleResumePauseEffectScope(lifecycleOwner.lifecycle) + } + LifecycleResumeEffectImpl(lifecycleOwner, lifecycleResumePauseEffectScope, effects) +} + +/** + * Schedule a pair of effects to run when the [Lifecycle] receives either a + * [Lifecycle.Event.ON_RESUME] or [Lifecycle.Event.ON_PAUSE] (or any new unique + * value of [keys]). The ON_RESUME effect will be the body of the [effects] + * block and the ON_PAUSE effect will be within the + * (onPauseOrDispose clause)[LifecycleResumePauseEffectScope.onPauseOrDispose]: + * + * ``` + * LifecycleResumeEffect(lifecycleOwner) { + * // add ON_RESUME effect here + * + * onPauseOrDispose { + * // add clean up for work kicked off in the ON_RESUME effect here + * } + * } + * ``` + * + * A [LifecycleResumeEffect] **must** include an + * [onPauseOrDispose][LifecycleResumePauseEffectScope.onPauseOrDispose] clause as + * the final statement in its [effects] block. If your operation does not require + * an effect for _both_ [Lifecycle.Event.ON_RESUME] and [Lifecycle.Event.ON_PAUSE], + * a [LifecycleEventEffect] should be used instead. + * + * A [LifecycleResumeEffect]'s _key_ is a value that defines the identity of the effect. + * If a key changes, the [LifecycleResumeEffect] must + * [dispose][LifecycleResumePauseEffectScope.onPauseOrDispose] its current [effects] and + * reset by calling [effects] again. Examples of keys include: + * + * * Observable objects that the effect subscribes to + * * Unique request parameters to an operation that must cancel and retry if those parameters change + * + * This function uses a [Lifecycle.Observer] to listen for when [LifecycleResumeEffect] + * enters the composition and the effects will be launched when receiving a + * [Lifecycle.Event.ON_RESUME] or [Lifecycle.Event.ON_PAUSE] event, respectively. If the + * [LifecycleResumeEffect] leaves the composition prior to receiving an [Lifecycle.Event.ON_PAUSE] + * event, [onPauseOrDispose][LifecycleResumePauseEffectScope.onPauseOrDispose] will be called + * to clean up the work that was kicked off in the ON_RESUME effect. + * + * This function should **not** be used to launch tasks in response to callback + * events by way of storing callback data as a [Lifecycle.State] in a [MutableState]. + * Instead, see [currentStateAsState] to obtain a [State][State] + * that may be used to launch jobs in response to state changes. + * + * @param keys The unique values to trigger recomposition upon changes + * @param lifecycleOwner The lifecycle owner to attach an observer + * @param effects The effects to be launched when we receive the respective event callbacks + */ +@Composable +public fun LifecycleResumeEffect( + vararg keys: Any?, + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + effects: LifecycleResumePauseEffectScope.() -> LifecyclePauseOrDisposeEffectResult +) { + val lifecycleResumePauseEffectScope = remember(*keys, lifecycleOwner) { + LifecycleResumePauseEffectScope(lifecycleOwner.lifecycle) + } + LifecycleResumeEffectImpl(lifecycleOwner, lifecycleResumePauseEffectScope, effects) +} + +@Composable +private fun LifecycleResumeEffectImpl( + lifecycleOwner: LifecycleOwner, + scope: LifecycleResumePauseEffectScope, + effects: LifecycleResumePauseEffectScope.() -> LifecyclePauseOrDisposeEffectResult +) { + DisposableEffect(lifecycleOwner, scope) { + var effectResult: LifecyclePauseOrDisposeEffectResult? = null + + val cancellable = lifecycleOwner.lifecycle.subscribe { event -> + when (event) { + Lifecycle.Event.ON_RESUME -> with(scope) { + effectResult = effects() + } + + Lifecycle.Event.ON_PAUSE -> effectResult?.runPauseOrOnDisposeEffect() + + else -> {} + } + } + + onDispose { + cancellable.cancel() + effectResult?.runPauseOrOnDisposeEffect() + } + } +} + +/** + * Interface used for [LifecycleResumeEffect] to run the effect within the onPauseOrDispose + * clause when an (ON_PAUSE)[Lifecycle.Event.ON_PAUSE] event is received or when cleanup is + * * needed for the work that was kicked off in the ON_RESUME effect. + */ +public interface LifecyclePauseOrDisposeEffectResult { + public fun runPauseOrOnDisposeEffect() +} + +/** + * Receiver scope for [LifecycleResumeEffect] that offers the [onPauseOrDispose] clause to + * couple the ON_RESUME effect. This should be the last statement in any call to + * [LifecycleResumeEffect]. + * + * This scope is also a [LifecycleOwner] to allow access to the + * (lifecycle)[LifecycleResumePauseEffectScope.lifecycle] within the [onPauseOrDispose] clause. + * + * @param lifecycle The lifecycle being observed by this receiver scope + */ +public class LifecycleResumePauseEffectScope(override val lifecycle: Lifecycle) : LifecycleOwner { + /** + * Provide the [onPauseOrDisposeEffect] to the [LifecycleResumeEffect] to run when the observer + * receives an (ON_PAUSE)[Lifecycle.Event.ON_PAUSE] event or must undergo cleanup. + */ + public inline fun onPauseOrDispose( + crossinline onPauseOrDisposeEffect: LifecycleOwner.() -> Unit + ): LifecyclePauseOrDisposeEffectResult = object : LifecyclePauseOrDisposeEffectResult { + override fun runPauseOrOnDisposeEffect() { + onPauseOrDisposeEffect() + } + } +} diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/currentStateAsState.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/currentStateAsState.kt new file mode 100644 index 00000000..40ad380b --- /dev/null +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/currentStateAsState.kt @@ -0,0 +1,16 @@ +package com.hoc081098.solivagant.lifecycle.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import com.hoc081098.solivagant.lifecycle.Lifecycle +import kotlinx.coroutines.flow.StateFlow + +/** + * Collects values from the [Lifecycle.currentStateFlow] and represents its latest value via [State]. + * The [StateFlow.value] is used as an initial value. Every time there would be new value posted + * into the [StateFlow] the returned [State] will be updated causing recomposition of every + * [State.value] usage. + */ +@Composable +public fun Lifecycle.currentStateAsState(): State = currentStateFlow.collectAsState() From c9d47386a6e0d764dc72b5512b002f44cb006990 Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sun, 21 Jan 2024 16:21:54 +0700 Subject: [PATCH 20/28] compose + lifecycle --- lifecycle/api/android/lifecycle.api | 2 +- lifecycle/api/jvm/lifecycle.api | 2 +- .../compose/{LifecycleEffect.kt => lifecycleEffects.kt} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/{LifecycleEffect.kt => lifecycleEffects.kt} (100%) diff --git a/lifecycle/api/android/lifecycle.api b/lifecycle/api/android/lifecycle.api index e417d265..dcf64f75 100644 --- a/lifecycle/api/android/lifecycle.api +++ b/lifecycle/api/android/lifecycle.api @@ -119,7 +119,7 @@ public final class com/hoc081098/solivagant/lifecycle/compose/CurrentStateAsStat public static final fun currentStateAsState (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Landroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/State; } -public final class com/hoc081098/solivagant/lifecycle/compose/LifecycleEffectKt { +public final class com/hoc081098/solivagant/lifecycle/compose/LifecycleEffectsKt { public static final fun LifecycleEventEffect (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V public static final fun LifecycleResumeEffect (Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V public static final fun LifecycleResumeEffect (Ljava/lang/Object;Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V diff --git a/lifecycle/api/jvm/lifecycle.api b/lifecycle/api/jvm/lifecycle.api index f8eaa423..c96c23d3 100644 --- a/lifecycle/api/jvm/lifecycle.api +++ b/lifecycle/api/jvm/lifecycle.api @@ -112,7 +112,7 @@ public final class com/hoc081098/solivagant/lifecycle/compose/CurrentStateAsStat public static final fun currentStateAsState (Lcom/hoc081098/solivagant/lifecycle/Lifecycle;Landroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/State; } -public final class com/hoc081098/solivagant/lifecycle/compose/LifecycleEffectKt { +public final class com/hoc081098/solivagant/lifecycle/compose/LifecycleEffectsKt { public static final fun LifecycleEventEffect (Lcom/hoc081098/solivagant/lifecycle/Lifecycle$Event;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V public static final fun LifecycleResumeEffect (Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V public static final fun LifecycleResumeEffect (Ljava/lang/Object;Ljava/lang/Object;Lcom/hoc081098/solivagant/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/LifecycleEffect.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/lifecycleEffects.kt similarity index 100% rename from lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/LifecycleEffect.kt rename to lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/lifecycleEffects.kt From ed985b7ad6e9a368c83e6fed5f1a0ae47272cd6a Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sun, 21 Jan 2024 16:22:35 +0700 Subject: [PATCH 21/28] compose + lifecycle --- .../hoc081098/solivagant/lifecycle/compose/lifecycleEffects.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/lifecycleEffects.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/lifecycleEffects.kt index cb004263..9470e11b 100644 --- a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/lifecycleEffects.kt +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/lifecycleEffects.kt @@ -14,6 +14,7 @@ * limitations under the License. */ +@file:Suppress("TooManyFunctions") package com.hoc081098.solivagant.lifecycle.compose import androidx.compose.runtime.Composable From 6fac00249f80ca14d14551dd2b7fa99761e46af1 Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sun, 21 Jan 2024 16:22:46 +0700 Subject: [PATCH 22/28] compose + lifecycle --- .../lifecycle/compose/lifecycleEffects.kt | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/lifecycleEffects.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/lifecycleEffects.kt index 9470e11b..e3dffc0b 100644 --- a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/lifecycleEffects.kt +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/lifecycleEffects.kt @@ -15,6 +15,7 @@ */ @file:Suppress("TooManyFunctions") + package com.hoc081098.solivagant.lifecycle.compose import androidx.compose.runtime.Composable @@ -53,13 +54,13 @@ import com.hoc081098.solivagant.lifecycle.LocalLifecycleOwner public fun LifecycleEventEffect( event: Lifecycle.Event, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, - onEvent: () -> Unit + onEvent: () -> Unit, ) { if (event == Lifecycle.Event.ON_DESTROY) { throw IllegalArgumentException( "LifecycleEventEffect cannot be used to " + - "listen for Lifecycle.Event.ON_DESTROY, since Compose disposes of the " + - "composition before ON_DESTROY observers are invoked." + "listen for Lifecycle.Event.ON_DESTROY, since Compose disposes of the " + + "composition before ON_DESTROY observers are invoked.", ) } @@ -126,7 +127,7 @@ public fun LifecycleEventEffect( public fun LifecycleStartEffect( key1: Any?, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, - effects: LifecycleStartStopEffectScope.() -> LifecycleStopOrDisposeEffectResult + effects: LifecycleStartStopEffectScope.() -> LifecycleStopOrDisposeEffectResult, ) { val lifecycleStartStopEffectScope = remember(key1, lifecycleOwner) { LifecycleStartStopEffectScope(lifecycleOwner.lifecycle) @@ -187,7 +188,7 @@ public fun LifecycleStartEffect( key1: Any?, key2: Any?, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, - effects: LifecycleStartStopEffectScope.() -> LifecycleStopOrDisposeEffectResult + effects: LifecycleStartStopEffectScope.() -> LifecycleStopOrDisposeEffectResult, ) { val lifecycleStartStopEffectScope = remember(key1, key2, lifecycleOwner) { LifecycleStartStopEffectScope(lifecycleOwner.lifecycle) @@ -250,7 +251,7 @@ public fun LifecycleStartEffect( key2: Any?, key3: Any?, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, - effects: LifecycleStartStopEffectScope.() -> LifecycleStopOrDisposeEffectResult + effects: LifecycleStartStopEffectScope.() -> LifecycleStopOrDisposeEffectResult, ) { val lifecycleStartStopEffectScope = remember(key1, key2, key3, lifecycleOwner) { LifecycleStartStopEffectScope(lifecycleOwner.lifecycle) @@ -309,7 +310,7 @@ public fun LifecycleStartEffect( public fun LifecycleStartEffect( vararg keys: Any?, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, - effects: LifecycleStartStopEffectScope.() -> LifecycleStopOrDisposeEffectResult + effects: LifecycleStartStopEffectScope.() -> LifecycleStopOrDisposeEffectResult, ) { val lifecycleStartStopEffectScope = remember(*keys, lifecycleOwner) { LifecycleStartStopEffectScope(lifecycleOwner.lifecycle) @@ -321,7 +322,7 @@ public fun LifecycleStartEffect( private fun LifecycleStartEffectImpl( lifecycleOwner: LifecycleOwner, scope: LifecycleStartStopEffectScope, - effects: LifecycleStartStopEffectScope.() -> LifecycleStopOrDisposeEffectResult + effects: LifecycleStartStopEffectScope.() -> LifecycleStopOrDisposeEffectResult, ) { DisposableEffect(lifecycleOwner, scope) { var effectResult: LifecycleStopOrDisposeEffectResult? = null @@ -370,7 +371,7 @@ public class LifecycleStartStopEffectScope(override val lifecycle: Lifecycle) : * observer receives an (ON_STOP)[Lifecycle.Event.ON_STOP] event or must undergo cleanup. */ public inline fun onStopOrDispose( - crossinline onStopOrDisposeEffect: LifecycleOwner.() -> Unit + crossinline onStopOrDisposeEffect: LifecycleOwner.() -> Unit, ): LifecycleStopOrDisposeEffectResult = object : LifecycleStopOrDisposeEffectResult { override fun runStopOrDisposeEffect() { onStopOrDisposeEffect() @@ -429,7 +430,7 @@ public class LifecycleStartStopEffectScope(override val lifecycle: Lifecycle) : public fun LifecycleResumeEffect( key1: Any?, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, - effects: LifecycleResumePauseEffectScope.() -> LifecyclePauseOrDisposeEffectResult + effects: LifecycleResumePauseEffectScope.() -> LifecyclePauseOrDisposeEffectResult, ) { val lifecycleResumePauseEffectScope = remember(key1, lifecycleOwner) { LifecycleResumePauseEffectScope(lifecycleOwner.lifecycle) @@ -490,7 +491,7 @@ public fun LifecycleResumeEffect( key1: Any?, key2: Any?, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, - effects: LifecycleResumePauseEffectScope.() -> LifecyclePauseOrDisposeEffectResult + effects: LifecycleResumePauseEffectScope.() -> LifecyclePauseOrDisposeEffectResult, ) { val lifecycleResumePauseEffectScope = remember(key1, key2, lifecycleOwner) { LifecycleResumePauseEffectScope(lifecycleOwner.lifecycle) @@ -553,7 +554,7 @@ public fun LifecycleResumeEffect( key2: Any?, key3: Any?, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, - effects: LifecycleResumePauseEffectScope.() -> LifecyclePauseOrDisposeEffectResult + effects: LifecycleResumePauseEffectScope.() -> LifecyclePauseOrDisposeEffectResult, ) { val lifecycleResumePauseEffectScope = remember(key1, key2, key3, lifecycleOwner) { LifecycleResumePauseEffectScope(lifecycleOwner.lifecycle) @@ -612,7 +613,7 @@ public fun LifecycleResumeEffect( public fun LifecycleResumeEffect( vararg keys: Any?, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, - effects: LifecycleResumePauseEffectScope.() -> LifecyclePauseOrDisposeEffectResult + effects: LifecycleResumePauseEffectScope.() -> LifecyclePauseOrDisposeEffectResult, ) { val lifecycleResumePauseEffectScope = remember(*keys, lifecycleOwner) { LifecycleResumePauseEffectScope(lifecycleOwner.lifecycle) @@ -624,7 +625,7 @@ public fun LifecycleResumeEffect( private fun LifecycleResumeEffectImpl( lifecycleOwner: LifecycleOwner, scope: LifecycleResumePauseEffectScope, - effects: LifecycleResumePauseEffectScope.() -> LifecyclePauseOrDisposeEffectResult + effects: LifecycleResumePauseEffectScope.() -> LifecyclePauseOrDisposeEffectResult, ) { DisposableEffect(lifecycleOwner, scope) { var effectResult: LifecyclePauseOrDisposeEffectResult? = null @@ -673,7 +674,7 @@ public class LifecycleResumePauseEffectScope(override val lifecycle: Lifecycle) * receives an (ON_PAUSE)[Lifecycle.Event.ON_PAUSE] event or must undergo cleanup. */ public inline fun onPauseOrDispose( - crossinline onPauseOrDisposeEffect: LifecycleOwner.() -> Unit + crossinline onPauseOrDisposeEffect: LifecycleOwner.() -> Unit, ): LifecyclePauseOrDisposeEffectResult = object : LifecyclePauseOrDisposeEffectResult { override fun runPauseOrOnDisposeEffect() { onPauseOrDisposeEffect() From 6c0b1bdcb8dbe14453dca94c8c12a97c65d9bbdd Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sun, 21 Jan 2024 16:23:09 +0700 Subject: [PATCH 23/28] compose + lifecycle --- .../compose/collectAsStateWithLifecycle.kt | 16 ++++++++++++++++ .../lifecycle/compose/currentStateAsState.kt | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/collectAsStateWithLifecycle.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/collectAsStateWithLifecycle.kt index 305bb94f..eb73e400 100644 --- a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/collectAsStateWithLifecycle.kt +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/collectAsStateWithLifecycle.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.hoc081098.solivagant.lifecycle.compose import androidx.compose.runtime.Composable diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/currentStateAsState.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/currentStateAsState.kt index 40ad380b..d1f0e2e3 100644 --- a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/currentStateAsState.kt +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/currentStateAsState.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.hoc081098.solivagant.lifecycle.compose import androidx.compose.runtime.Composable From 7f9a480a508832e75efe645e3291696beede82bf Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sun, 21 Jan 2024 16:24:46 +0700 Subject: [PATCH 24/28] compose + lifecycle --- .../com/hoc081098/solivagant/sample/start/StartScreen.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/start/StartScreen.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/start/StartScreen.kt index d6f18d9a..86e8e206 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/start/StartScreen.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/start/StartScreen.kt @@ -16,12 +16,18 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.hoc081098.kmp.viewmodel.koin.compose.koinKmpViewModel import com.hoc081098.solivagant.lifecycle.LocalLifecycleOwner +import com.hoc081098.solivagant.lifecycle.compose.LifecycleResumeEffect @Composable internal fun StartScreen( modifier: Modifier = Modifier, viewModel: StartViewModel = koinKmpViewModel(), ) { + LifecycleResumeEffect(Unit) { + println(">>> LifecycleResumeEffect run") + onPauseOrDispose { println(">>> LifecycleResumeEffect onPauseOrDispose") } + } + LocalLifecycleOwner.current.let { owner -> LaunchedEffect(owner) { owner.lifecycle.currentStateFlow.collect { From 28decae87616dc18eeed367bcb32314c08bf45bf Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sun, 21 Jan 2024 16:31:35 +0700 Subject: [PATCH 25/28] credits --- .../solivagant/lifecycle/Lifecycle.kt | 32 +++++++++++++++++++ .../solivagant/lifecycle/LifecycleOwner.kt | 32 +++++++++++++++++++ .../lifecycle/LifecycleOwnerRegistry.kt | 16 ++++++++++ .../compose/collectAsStateWithLifecycle.kt | 2 +- .../solivagant/lifecycle/flowWithLifecycle.kt | 16 ++++++++++ .../solivagant/lifecycle/repeatOnLifecycle.kt | 16 ++++++++++ .../lifecycle/withLifecycleState.kt | 16 ++++++++++ 7 files changed, 129 insertions(+), 1 deletion(-) diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/Lifecycle.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/Lifecycle.kt index b2f11038..654e9d20 100644 --- a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/Lifecycle.kt +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/Lifecycle.kt @@ -1,3 +1,35 @@ +/* + * Copyright 2024 Arkadii Ivanov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.hoc081098.solivagant.lifecycle import kotlin.jvm.JvmStatic diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LifecycleOwner.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LifecycleOwner.kt index dc7646b2..b5d52722 100644 --- a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LifecycleOwner.kt +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LifecycleOwner.kt @@ -1,3 +1,35 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright 2024 Arkadii Ivanov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.hoc081098.solivagant.lifecycle /** diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LifecycleOwnerRegistry.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LifecycleOwnerRegistry.kt index 08bf128e..4cdf61c2 100644 --- a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LifecycleOwnerRegistry.kt +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/LifecycleOwnerRegistry.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2024 Arkadii Ivanov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.hoc081098.solivagant.lifecycle import com.hoc081098.solivagant.lifecycle.Lifecycle.Cancellable diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/collectAsStateWithLifecycle.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/collectAsStateWithLifecycle.kt index eb73e400..89ce8501 100644 --- a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/collectAsStateWithLifecycle.kt +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/compose/collectAsStateWithLifecycle.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Android Open Source Project + * Copyright 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/flowWithLifecycle.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/flowWithLifecycle.kt index 0a23d386..84be8509 100644 --- a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/flowWithLifecycle.kt +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/flowWithLifecycle.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.hoc081098.solivagant.lifecycle import kotlinx.coroutines.flow.Flow diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/repeatOnLifecycle.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/repeatOnLifecycle.kt index f6a98b01..33ad1520 100644 --- a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/repeatOnLifecycle.kt +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/repeatOnLifecycle.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.hoc081098.solivagant.lifecycle import com.hoc081098.solivagant.lifecycle.Lifecycle.Cancellable diff --git a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/withLifecycleState.kt b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/withLifecycleState.kt index 6e8cd29b..8ad31e0a 100644 --- a/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/withLifecycleState.kt +++ b/lifecycle/src/commonMain/kotlin/com/hoc081098/solivagant/lifecycle/withLifecycleState.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.hoc081098.solivagant.lifecycle import com.hoc081098.solivagant.lifecycle.Lifecycle.Companion.currentState From 192e65a07c5aec2884d9601faac0debfbb0318a6 Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sun, 21 Jan 2024 16:33:12 +0700 Subject: [PATCH 26/28] delete iosApp for now --- sample/iosApp/.gitignore | 123 --- sample/iosApp/Podfile | 19 - sample/iosApp/Podfile.lock | 57 -- .../iosApp/iosApp-RxSwift/AppDelegate.swift | 36 - .../AccentColor.colorset/Contents.json | 11 - .../AppIcon.appiconset/Contents.json | 98 --- .../Assets.xcassets/Contents.json | 6 - .../Base.lproj/LaunchScreen.storyboard | 32 - .../iosApp-RxSwift/Base.lproj/Main.storyboard | 124 --- sample/iosApp/iosApp-RxSwift/Info.plist | 25 - .../Products/IosProductsViewModel.swift | 68 -- .../Products/ProductsViewController.swift | 108 --- .../iosApp/iosApp-RxSwift/SceneDelegate.swift | 53 -- .../Utils/DIContainer+get.swift | 24 - .../iosApp/iosApp-RxSwift/Utils/Napier.swift | 152 ---- .../Utils/kotlinxCoroutinesFlow+RxSwift.swift | 59 -- .../iosApp/iosApp.xcodeproj/project.pbxproj | 782 ------------------ .../xcshareddata/xcschemes/iosApp.xcscheme | 78 -- .../contents.xcworkspacedata | 10 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../AccentColor.colorset/Contents.json | 11 - .../AppIcon.appiconset/Contents.json | 98 --- .../iosApp/Assets.xcassets/Contents.json | 6 - sample/iosApp/iosApp/CommonUi.swift | 88 -- sample/iosApp/iosApp/Info.plist | 48 -- sample/iosApp/iosApp/Others/async_let.swift | 40 - .../iosApp/Others/callback_to_async.swift | 90 -- .../Preview Assets.xcassets/Contents.json | 6 - .../IosProductDetailViewModel.swift | 42 - .../Product Detail/ProductDetailView.swift | 144 ---- .../Products/IosProductsViewModel.swift | 85 -- .../iosApp/iosApp/Products/ProductsView.swift | 81 -- .../IosSearchProductsViewModel.swift | 52 -- .../Search Products/SearchProductsView.swift | 60 -- .../iosApp/iosApp/Snippets/SnippetsView.swift | 76 -- .../iosApp/iosApp/Utils/DIContainer+get.swift | 24 - sample/iosApp/iosApp/Utils/Napier.swift | 152 ---- .../iosApp/iosApp/Utils/String+toDate.swift | 106 --- .../kotlinxCoroutinesFlowExtensions.swift | 161 ---- sample/iosApp/iosApp/iOSApp.swift | 42 - sample/shared/build.gradle.kts | 18 - sample/shared/shared.podspec | 50 -- 42 files changed, 3353 deletions(-) delete mode 100644 sample/iosApp/.gitignore delete mode 100644 sample/iosApp/Podfile delete mode 100644 sample/iosApp/Podfile.lock delete mode 100644 sample/iosApp/iosApp-RxSwift/AppDelegate.swift delete mode 100644 sample/iosApp/iosApp-RxSwift/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 sample/iosApp/iosApp-RxSwift/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 sample/iosApp/iosApp-RxSwift/Assets.xcassets/Contents.json delete mode 100644 sample/iosApp/iosApp-RxSwift/Base.lproj/LaunchScreen.storyboard delete mode 100644 sample/iosApp/iosApp-RxSwift/Base.lproj/Main.storyboard delete mode 100644 sample/iosApp/iosApp-RxSwift/Info.plist delete mode 100644 sample/iosApp/iosApp-RxSwift/Products/IosProductsViewModel.swift delete mode 100644 sample/iosApp/iosApp-RxSwift/Products/ProductsViewController.swift delete mode 100644 sample/iosApp/iosApp-RxSwift/SceneDelegate.swift delete mode 100644 sample/iosApp/iosApp-RxSwift/Utils/DIContainer+get.swift delete mode 100644 sample/iosApp/iosApp-RxSwift/Utils/Napier.swift delete mode 100644 sample/iosApp/iosApp-RxSwift/Utils/kotlinxCoroutinesFlow+RxSwift.swift delete mode 100644 sample/iosApp/iosApp.xcodeproj/project.pbxproj delete mode 100644 sample/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme delete mode 100644 sample/iosApp/iosApp.xcworkspace/contents.xcworkspacedata delete mode 100644 sample/iosApp/iosApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 sample/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 sample/iosApp/iosApp/Assets.xcassets/Contents.json delete mode 100644 sample/iosApp/iosApp/CommonUi.swift delete mode 100644 sample/iosApp/iosApp/Info.plist delete mode 100644 sample/iosApp/iosApp/Others/async_let.swift delete mode 100644 sample/iosApp/iosApp/Others/callback_to_async.swift delete mode 100644 sample/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json delete mode 100644 sample/iosApp/iosApp/Product Detail/IosProductDetailViewModel.swift delete mode 100644 sample/iosApp/iosApp/Product Detail/ProductDetailView.swift delete mode 100644 sample/iosApp/iosApp/Products/IosProductsViewModel.swift delete mode 100644 sample/iosApp/iosApp/Products/ProductsView.swift delete mode 100644 sample/iosApp/iosApp/Search Products/IosSearchProductsViewModel.swift delete mode 100644 sample/iosApp/iosApp/Search Products/SearchProductsView.swift delete mode 100644 sample/iosApp/iosApp/Snippets/SnippetsView.swift delete mode 100644 sample/iosApp/iosApp/Utils/DIContainer+get.swift delete mode 100644 sample/iosApp/iosApp/Utils/Napier.swift delete mode 100644 sample/iosApp/iosApp/Utils/String+toDate.swift delete mode 100644 sample/iosApp/iosApp/Utils/kotlinxCoroutinesFlowExtensions.swift delete mode 100644 sample/iosApp/iosApp/iOSApp.swift delete mode 100644 sample/shared/shared.podspec diff --git a/sample/iosApp/.gitignore b/sample/iosApp/.gitignore deleted file mode 100644 index 73b137d3..00000000 --- a/sample/iosApp/.gitignore +++ /dev/null @@ -1,123 +0,0 @@ -# Created by https://www.toptal.com/developers/gitignore/api/macos,xcode,objective-c,cocoapods -# Edit at https://www.toptal.com/developers/gitignore?templates=macos,xcode,objective-c,cocoapods - -### CocoaPods ### -## CocoaPods GitIgnore Template - -# CocoaPods - Only use to conserve bandwidth / Save time on Pushing -# - Also handy if you have a large number of dependant pods -# - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE -Pods/ - -### macOS ### -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -### macOS Patch ### -# iCloud generated files -*.icloud - -### Objective-C ### -# Xcode -# -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore - -## User settings -xcuserdata/ - -## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) -*.xcscmblueprint -*.xccheckout - -## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) -build/ -DerivedData/ -*.moved-aside -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 - -## Obj-C/Swift specific -*.hmap - -## App packaging -*.ipa -*.dSYM.zip -*.dSYM - -# CocoaPods -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control -# Pods/ -# Add this line if you want to avoid checking in source code from the Xcode workspace -# *.xcworkspace - -# Carthage -# Add this line if you want to avoid checking in source code from Carthage dependencies. -# Carthage/Checkouts - -Carthage/Build/ - -# fastlane -# It is recommended to not store the screenshots in the git repo. -# Instead, use fastlane to re-generate the screenshots whenever they are needed. -# For more information about the recommended setup visit: -# https://docs.fastlane.tools/best-practices/source-control/#source-control - -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots/**/*.png -fastlane/test_output - -# Code Injection -# After new code Injection tools there's a generated folder /iOSInjectionProject -# https://github.com/johnno1962/injectionforxcode - -iOSInjectionProject/ - -### Objective-C Patch ### - -### Xcode ### - -## Xcode 8 and earlier - -### Xcode Patch ### -*.xcodeproj/* -!*.xcodeproj/project.pbxproj -!*.xcodeproj/xcshareddata/ -!*.xcworkspace/contents.xcworkspacedata -/*.gcno -**/xcshareddata/WorkspaceSettings.xcsettings - -# End of https://www.toptal.com/developers/gitignore/api/macos,xcode,objective-c,cocoapods diff --git a/sample/iosApp/Podfile b/sample/iosApp/Podfile deleted file mode 100644 index 4c41b60f..00000000 --- a/sample/iosApp/Podfile +++ /dev/null @@ -1,19 +0,0 @@ -target 'iosApp' do - use_frameworks! - platform :ios, '14.1' - pod 'shared', :path => '../shared' - pod 'Kingfisher', '~> 7.0' -end - -target 'iosApp-RxSwift' do - use_frameworks! - platform :ios, '14.1' - pod 'shared', :path => '../shared' - pod 'Kingfisher', '~> 7.0' - pod 'RxSwift', '~> 6.6.0' - pod 'RxCocoa', '~> 6.6.0' - pod 'RxRelay', '~> 6.6.0' - pod 'NSObject+Rx', '~> 5.2.2' - pod 'RxDataSources', '~> 5.0' - pod 'MBProgressHUD', '~> 1.2.0' -end diff --git a/sample/iosApp/Podfile.lock b/sample/iosApp/Podfile.lock deleted file mode 100644 index 3f522503..00000000 --- a/sample/iosApp/Podfile.lock +++ /dev/null @@ -1,57 +0,0 @@ -PODS: - - Differentiator (5.0.0) - - Kingfisher (7.4.0) - - MBProgressHUD (1.2.0) - - "NSObject+Rx (5.2.2)": - - RxSwift (~> 6.2) - - RxCocoa (6.6.0): - - RxRelay (= 6.6.0) - - RxSwift (= 6.6.0) - - RxDataSources (5.0.0): - - Differentiator (~> 5.0) - - RxCocoa (~> 6.0) - - RxSwift (~> 6.0) - - RxRelay (6.6.0): - - RxSwift (= 6.6.0) - - RxSwift (6.6.0) - - shared (1.0) - -DEPENDENCIES: - - Kingfisher (~> 7.0) - - MBProgressHUD (~> 1.2.0) - - "NSObject+Rx (~> 5.2.2)" - - RxCocoa (~> 6.6.0) - - RxDataSources (~> 5.0) - - RxRelay (~> 6.6.0) - - RxSwift (~> 6.6.0) - - shared (from `../shared`) - -SPEC REPOS: - trunk: - - Differentiator - - Kingfisher - - MBProgressHUD - - "NSObject+Rx" - - RxCocoa - - RxDataSources - - RxRelay - - RxSwift - -EXTERNAL SOURCES: - shared: - :path: "../shared" - -SPEC CHECKSUMS: - Differentiator: e8497ceab83c1b10ca233716d547b9af21b9344d - Kingfisher: 596b207973051aae5e4c54a1c1cb39837ea10d36 - MBProgressHUD: 3ee5efcc380f6a79a7cc9b363dd669c5e1ae7406 - "NSObject+Rx": 61cf1f7306a73dcef8b36649198af0813ec18dfd - RxCocoa: 44a80de90e25b739b5aeaae3c8c371a32e3343cc - RxDataSources: aa47cc1ed6c500fa0dfecac5c979b723542d79cf - RxRelay: 45eaa5db8ee4fb50e5ebd57deec0159e97fa51e6 - RxSwift: a4b44f7d24599f674deebd1818eab82e58410632 - shared: 8b0dbc7374b73d572d2b95d5fcf0c9c76801ff09 - -PODFILE CHECKSUM: ba90cf8daf1f3b2d6cf7e270921fbb887b46843e - -COCOAPODS: 1.12.1 diff --git a/sample/iosApp/iosApp-RxSwift/AppDelegate.swift b/sample/iosApp/iosApp-RxSwift/AppDelegate.swift deleted file mode 100644 index 5ae94cd0..00000000 --- a/sample/iosApp/iosApp-RxSwift/AppDelegate.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// AppDelegate.swift -// iosApp-RxSwift -// -// Created by Petrus Nguyen Thai Hoc on 4/11/23. -// Copyright © 2023 orgName. All rights reserved. -// - -import UIKit -import shared - -@main -class AppDelegate: UIResponder, UIApplicationDelegate { - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - - DIContainer.shared.doInit { _ in } - - return true - } - - // MARK: UISceneSession Lifecycle - - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - } -} - diff --git a/sample/iosApp/iosApp-RxSwift/Assets.xcassets/AccentColor.colorset/Contents.json b/sample/iosApp/iosApp-RxSwift/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb878970..00000000 --- a/sample/iosApp/iosApp-RxSwift/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/sample/iosApp/iosApp-RxSwift/Assets.xcassets/AppIcon.appiconset/Contents.json b/sample/iosApp/iosApp-RxSwift/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 9221b9bb..00000000 --- a/sample/iosApp/iosApp-RxSwift/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "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 - } -} diff --git a/sample/iosApp/iosApp-RxSwift/Assets.xcassets/Contents.json b/sample/iosApp/iosApp-RxSwift/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596..00000000 --- a/sample/iosApp/iosApp-RxSwift/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/sample/iosApp/iosApp-RxSwift/Base.lproj/LaunchScreen.storyboard b/sample/iosApp/iosApp-RxSwift/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f25e499e..00000000 --- a/sample/iosApp/iosApp-RxSwift/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/sample/iosApp/iosApp-RxSwift/Base.lproj/Main.storyboard b/sample/iosApp/iosApp-RxSwift/Base.lproj/Main.storyboard deleted file mode 100644 index aded58e9..00000000 --- a/sample/iosApp/iosApp-RxSwift/Base.lproj/Main.storyboard +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/sample/iosApp/iosApp-RxSwift/Info.plist b/sample/iosApp/iosApp-RxSwift/Info.plist deleted file mode 100644 index dd3c9afd..00000000 --- a/sample/iosApp/iosApp-RxSwift/Info.plist +++ /dev/null @@ -1,25 +0,0 @@ - - - - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main - - - - - - diff --git a/sample/iosApp/iosApp-RxSwift/Products/IosProductsViewModel.swift b/sample/iosApp/iosApp-RxSwift/Products/IosProductsViewModel.swift deleted file mode 100644 index 83e05fe4..00000000 --- a/sample/iosApp/iosApp-RxSwift/Products/IosProductsViewModel.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// IosProductsViewModel.swift -// iosApp-RxSwift -// -// Created by Petrus Nguyen Thai Hoc on 4/11/23. -// Copyright © 2023 orgName. All rights reserved. -// - -import Foundation -import RxSwift -import RxCocoa -import shared - -class IosProductsViewModel { - private let commonVm = DIContainer.shared.get(for: ProductsViewModel.self) - - let state$: Driver - - init() { - self.state$ = self.commonVm - .stateFlow - .asNonNullObservable( - ProductsState.self, - dispatcher: DIContainer.shared - .get(for: AppDispatchers.self) - .immediateMain - ) - .asDriver(onErrorDriveWith: .empty()) - } - - func dispatch(action: ProductsAction) { - self.commonVm.dispatch(action: action) - } - - deinit { - self.commonVm.clear() - Napier.d("\(self)::deinit") - } - - // MARK: Demo purpose only - - private let disposable = SerialDisposable() - - private lazy var ticker$: Observable = self.commonVm - .tickerFlow - .asNullableObservable( - NSString.self, - dispatcher: DIContainer.shared - .get(for: AppDispatchers.self) - .immediateMain - ) - - func onActive() { - Napier.d("onActive") - self.disposable.disposable = self - .ticker$ - .subscribe( - onNext: { v in Napier.d("[ticker]: value=\(v ?? "nil")") }, - onCompleted: { Napier.d("[ticker]: finished") }, - onDisposed: { Napier.d("[ticker]: disposed") } - ) - } - - func onInactive() { - Napier.d("onInactive") - self.disposable.dispose() - } -} diff --git a/sample/iosApp/iosApp-RxSwift/Products/ProductsViewController.swift b/sample/iosApp/iosApp-RxSwift/Products/ProductsViewController.swift deleted file mode 100644 index f02b7d73..00000000 --- a/sample/iosApp/iosApp-RxSwift/Products/ProductsViewController.swift +++ /dev/null @@ -1,108 +0,0 @@ -// -// ViewController.swift -// iosApp-RxSwift -// -// Created by Petrus Nguyen Thai Hoc on 4/11/23. -// Copyright © 2023 orgName. All rights reserved. -// - -import UIKit -import shared -import RxSwift -import NSObject_Rx -import Kingfisher -import RxRelay -import RxDataSources -import MBProgressHUD - -class ProductTableViewCell: UITableViewCell { - - @IBOutlet weak var descriptionLabel: UILabel! - @IBOutlet weak var titleLabel: UILabel! - @IBOutlet weak var productImageView: UIImageView! - - func configure(with item: ProductItemUi) { - let url = item.images.first.flatMap(URL.init(string:)) - - KF.url(url) - .cacheOriginalImage() - .onFailure { e in Napier.e(error: e, "err: url=\(String(describing: url)), e=\(e)") } - .fade(duration: 1) - .forceTransition() - .set(to: productImageView) - - titleLabel.text = item.title - descriptionLabel.text = item.description_ - } -} - -class ProductsViewController: UIViewController { - - private let vm = IosProductsViewModel() - - @IBOutlet weak var tableView: UITableView! - - override func viewDidLoad() { - super.viewDidLoad() - - self.tableView.estimatedRowHeight = 72 + 16 * 2 - self.tableView.rowHeight = UITableView.automaticDimension - - bindVm() - - // Auto retry on error - self.vm.state$ - .map(\.error) - .distinctUntilChanged() - .filter { $0 != nil } - .map { _ in ProductsActionLoad() } - .startWith(ProductsActionLoad()) - .drive(onNext: vm.dispatch) - .disposed(by: self.rx.disposeBag) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - self.vm.onActive() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - self.vm.onInactive() - } - - deinit { - Napier.d("\(self)::deinit") - } -} - -private extension ProductsViewController { - func bindVm() { - let dataSource = RxTableViewSectionedAnimatedDataSource> { dataSource, tableView, indexPath, item in - let cell = tableView.dequeueReusableCell(withIdentifier: "ProductTableViewCell", for: indexPath) as! ProductTableViewCell - cell.configure(with: item) - return cell - } - - self.vm.state$ - .map { state in [.init(model: "", items: state.products)] } - .drive(self.tableView.rx.items(dataSource: dataSource)) - .disposed(by: self.rx.disposeBag) - - self.vm.state$ - .map(\.isLoading) - .distinctUntilChanged() - .drive(with: self, onNext: { vc, v in - if v { - MBProgressHUD.showAdded(to: vc.view, animated: true) - } else { - MBProgressHUD.hide(for: vc.view, animated: true) - } - }) - .disposed(by: self.rx.disposeBag) - } -} - -extension ProductItemUi: IdentifiableType { - public var identity: Int32 { self.id } -} diff --git a/sample/iosApp/iosApp-RxSwift/SceneDelegate.swift b/sample/iosApp/iosApp-RxSwift/SceneDelegate.swift deleted file mode 100644 index 87e16f7e..00000000 --- a/sample/iosApp/iosApp-RxSwift/SceneDelegate.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// SceneDelegate.swift -// iosApp-RxSwift -// -// Created by Petrus Nguyen Thai Hoc on 4/11/23. -// Copyright © 2023 orgName. All rights reserved. -// - -import UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - - var window: UIWindow? - - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - } - - -} - diff --git a/sample/iosApp/iosApp-RxSwift/Utils/DIContainer+get.swift b/sample/iosApp/iosApp-RxSwift/Utils/DIContainer+get.swift deleted file mode 100644 index 919f04b8..00000000 --- a/sample/iosApp/iosApp-RxSwift/Utils/DIContainer+get.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// DIContainer+get.swift -// iosApp -// -// Created by Petrus Nguyen Thai Hoc on 2/12/23. -// Copyright © 2023 orgName. All rights reserved. -// - -import Foundation -import shared - -extension DIContainer { - func get( - for type: T.Type = T.self, - qualifier: Koin_coreQualifier? = nil, - parameters: (() -> Koin_coreParametersHolder)? = nil - ) -> T { - self.get( - type: type, - qualifier: qualifier, - parameters: parameters - ) as! T - } -} diff --git a/sample/iosApp/iosApp-RxSwift/Utils/Napier.swift b/sample/iosApp/iosApp-RxSwift/Utils/Napier.swift deleted file mode 100644 index ebeb3cb3..00000000 --- a/sample/iosApp/iosApp-RxSwift/Utils/Napier.swift +++ /dev/null @@ -1,152 +0,0 @@ -// -// Napier.swift -// iosApp -// -// Created by Petrus Nguyen Thai Hoc on 2/12/23. -// Copyright © 2023 orgName. All rights reserved. -// - -import Foundation -import shared - -extension Napier { - static func v( - tag: String? = nil, - error: Error? = nil, - _ items: Any..., - separator: String = " ", - file: String = #file, - function: String = #function - ) { - log( - logLevel: .verbose, - error: error, - tag: tag, - items, - separator: separator, - file: file, - function: function) - } - - static func d( - tag: String? = nil, - error: Error? = nil, - _ items: Any..., - separator: String = " ", - file: String = #file, - function: String = #function - ) { - log( - logLevel: .debug, - error: error, - tag: tag, - items, - separator: separator, - file: file, - function: function) - } - - static func i( - tag: String? = nil, - error: Error? = nil, - _ items: Any..., - separator: String = " ", - file: String = #file, - function: String = #function - ) { - log( - logLevel: .info, - error: error, - tag: tag, - items, - separator: separator, - file: file, - function: function) - } - - static func w( - tag: String? = nil, - error: Error? = nil, - _ items: Any..., - separator: String = " ", - file: String = #file, - function: String = #function - ) { - log( - logLevel: .warning, - error: error, - tag: tag, - items, - separator: separator, - file: file, - function: function) - } - - static func e( - tag: String? = nil, - error: Error? = nil, - _ items: Any..., - separator: String = " ", - file: String = #file, - function: String = #function - ) { - log( - logLevel: .error, - error: error, - tag: tag, - items, - separator: separator, - file: file, - function: function) - } - - static func a( - tag: String? = nil, - error: Error? = nil, - _ items: Any..., - separator: String = " ", - file: String = #file, - function: String = #function - ) { - log( - logLevel: .assert, - error: error, - tag: tag, - items, - separator: separator, - file: file, - function: function) - } - - static private func log( - logLevel: LogLevel, - error: Error?, - tag: String?, - _ items: [Any], - separator: String, - file: String, - function: String - ) { - let message = items.map { "\($0)" }.joined(separator: separator) - - let throwable: KotlinThrowable? - if let error = error { - throwable = NSErrorKt.asThrowable(error) - } else { - throwable = nil - } - - Self.shared.log( - priority: logLevel, - tag: tag ?? Self.defaultTag(file: file, function: function), - throwable: throwable, - message: message - ) - } - - private static func defaultTag(file: String, function: String) -> String { - let fileName = URL(fileURLWithPath: file).lastPathComponent - let functionName = String(function.prefix(while: { $0 != "(" })) - return "\(fileName):\(functionName)" - } -} diff --git a/sample/iosApp/iosApp-RxSwift/Utils/kotlinxCoroutinesFlow+RxSwift.swift b/sample/iosApp/iosApp-RxSwift/Utils/kotlinxCoroutinesFlow+RxSwift.swift deleted file mode 100644 index 3136ed6e..00000000 --- a/sample/iosApp/iosApp-RxSwift/Utils/kotlinxCoroutinesFlow+RxSwift.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// kotlinxCoroutinesFlow+RxSwift.swift -// iosApp -// -// Created by Petrus Nguyen Thai Hoc on 2/12/23. -// Copyright © 2023 orgName. All rights reserved. -// - -import Foundation -import shared -import RxSwift - -private func supervisorJobScope(dispatcher: CoroutineDispatcher) -> CoroutineScope { - CoroutineScopeKt.CoroutineScope( - context: dispatcher.plus(context: SupervisorKt.SupervisorJob(parent: nil)) - ) -} - -extension Flow { - // MARK: - Flow - - func asNonNullObservable( - _ type: T.Type = T.self, - dispatcher: CoroutineDispatcher = Dispatchers.shared.Unconfined - ) -> Observable { - Observable.create { observer in - let wrapper = NonNullFlowWrapperKt.wrap(self) as! NonNullFlowWrapper - - let closable = wrapper.subscribe( - scope: supervisorJobScope(dispatcher: dispatcher), - onValue: observer.onNext, - onError: { t in observer.onError(t.asNSError()) }, - onComplete: observer.onCompleted - ) - - return Disposables.create(with: closable.close) - } - } - - // MARK: - Flow - - func asNullableObservable( - _ type: T.Type = T.self, - dispatcher: CoroutineDispatcher = Dispatchers.shared.Unconfined - ) -> Observable { - Observable.create { observer in - let wrapper = NullableFlowWrapperKt.wrap(self) as! NullableFlowWrapper - - let closable = wrapper.subscribe( - scope: supervisorJobScope(dispatcher: dispatcher), - onValue: observer.onNext, - onError: { t in observer.onError(t.asNSError()) }, - onComplete: observer.onCompleted - ) - - return Disposables.create(with: closable.close) - } - } -} diff --git a/sample/iosApp/iosApp.xcodeproj/project.pbxproj b/sample/iosApp/iosApp.xcodeproj/project.pbxproj deleted file mode 100644 index 9347408b..00000000 --- a/sample/iosApp/iosApp.xcodeproj/project.pbxproj +++ /dev/null @@ -1,782 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 51; - 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 */; }; - 352D59F02998EDD3008142AF /* kotlinxCoroutinesFlowExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 352D59EF2998EDD3008142AF /* kotlinxCoroutinesFlowExtensions.swift */; }; - 352D59F22998EE6C008142AF /* Napier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 352D59F12998EE6C008142AF /* Napier.swift */; }; - 352D59F42998F119008142AF /* IosProductsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 352D59F32998F119008142AF /* IosProductsViewModel.swift */; }; - 352D59F72998F1A4008142AF /* DIContainer+get.swift in Sources */ = {isa = PBXBuildFile; fileRef = 352D59F62998F1A4008142AF /* DIContainer+get.swift */; }; - 354544A829BA2C6B003FE581 /* SnippetsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 354544A729BA2C6B003FE581 /* SnippetsView.swift */; }; - 35E34EBE29BCDDD300ED7D75 /* async_let.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35E34EBD29BCDDD300ED7D75 /* async_let.swift */; }; - 35E34EC129BDE02F00ED7D75 /* callback_to_async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35E34EC029BDE02F00ED7D75 /* callback_to_async.swift */; }; - 35F5E36329AA18EC00EE8FF8 /* SearchProductsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F5E36229AA18EC00EE8FF8 /* SearchProductsView.swift */; }; - 35F5E36529AA191F00EE8FF8 /* CommonUi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F5E36429AA191F00EE8FF8 /* CommonUi.swift */; }; - 35F5E36729AA19D700EE8FF8 /* IosSearchProductsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F5E36629AA19D700EE8FF8 /* IosSearchProductsViewModel.swift */; }; - 35F5E36A29B397B000EE8FF8 /* ProductDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F5E36929B397B000EE8FF8 /* ProductDetailView.swift */; }; - 35F5E36C29B3996500EE8FF8 /* IosProductDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F5E36B29B3996500EE8FF8 /* IosProductDetailViewModel.swift */; }; - 35F5E36E29B3B6EB00EE8FF8 /* String+toDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F5E36D29B3B6EB00EE8FF8 /* String+toDate.swift */; }; - 35F8386429E52B5600CC2335 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F8386329E52B5600CC2335 /* AppDelegate.swift */; }; - 35F8386629E52B5600CC2335 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F8386529E52B5600CC2335 /* SceneDelegate.swift */; }; - 35F8386829E52B5600CC2335 /* ProductsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F8386729E52B5600CC2335 /* ProductsViewController.swift */; }; - 35F8386B29E52B5600CC2335 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 35F8386929E52B5600CC2335 /* Main.storyboard */; }; - 35F8386D29E52B5D00CC2335 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 35F8386C29E52B5D00CC2335 /* Assets.xcassets */; }; - 35F8387029E52B5D00CC2335 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 35F8386E29E52B5D00CC2335 /* LaunchScreen.storyboard */; }; - 35F8388129E52DCE00CC2335 /* Napier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F8388029E52DCE00CC2335 /* Napier.swift */; }; - 35F8388329E52DE800CC2335 /* DIContainer+get.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F8388229E52DE800CC2335 /* DIContainer+get.swift */; }; - 35F8388729E52E1300CC2335 /* kotlinxCoroutinesFlow+RxSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F8388629E52E1300CC2335 /* kotlinxCoroutinesFlow+RxSwift.swift */; }; - 35F8388929E5375300CC2335 /* IosProductsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F8388829E5375300CC2335 /* IosProductsViewModel.swift */; }; - 64DE2C15350EA835D95C1039 /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2E8E8ABD96E9CB70C7023D30 /* Pods_iosApp.framework */; }; - 7555FF83242A565900829871 /* ProductsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ProductsView.swift */; }; - F7C477ABF007F86120032AB8 /* Pods_iosApp_RxSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A843F298B8B7BBF27ADB620 /* Pods_iosApp_RxSwift.framework */; }; -/* End PBXBuildFile 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 = ""; }; - 2E8E8ABD96E9CB70C7023D30 /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 320FC384F13C505E6AB66632 /* Pods-iosApp-RxSwift.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp-RxSwift.debug.xcconfig"; path = "Target Support Files/Pods-iosApp-RxSwift/Pods-iosApp-RxSwift.debug.xcconfig"; sourceTree = ""; }; - 352D59EF2998EDD3008142AF /* kotlinxCoroutinesFlowExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = kotlinxCoroutinesFlowExtensions.swift; sourceTree = ""; }; - 352D59F12998EE6C008142AF /* Napier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Napier.swift; sourceTree = ""; }; - 352D59F32998F119008142AF /* IosProductsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosProductsViewModel.swift; sourceTree = ""; }; - 352D59F62998F1A4008142AF /* DIContainer+get.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DIContainer+get.swift"; sourceTree = ""; }; - 354544A729BA2C6B003FE581 /* SnippetsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnippetsView.swift; sourceTree = ""; }; - 35E34EBD29BCDDD300ED7D75 /* async_let.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = async_let.swift; sourceTree = ""; }; - 35E34EC029BDE02F00ED7D75 /* callback_to_async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = callback_to_async.swift; sourceTree = ""; }; - 35F5E36229AA18EC00EE8FF8 /* SearchProductsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchProductsView.swift; sourceTree = ""; }; - 35F5E36429AA191F00EE8FF8 /* CommonUi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonUi.swift; sourceTree = ""; }; - 35F5E36629AA19D700EE8FF8 /* IosSearchProductsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosSearchProductsViewModel.swift; sourceTree = ""; }; - 35F5E36929B397B000EE8FF8 /* ProductDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailView.swift; sourceTree = ""; }; - 35F5E36B29B3996500EE8FF8 /* IosProductDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosProductDetailViewModel.swift; sourceTree = ""; }; - 35F5E36D29B3B6EB00EE8FF8 /* String+toDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+toDate.swift"; sourceTree = ""; }; - 35F8386129E52B5600CC2335 /* iosApp-RxSwift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "iosApp-RxSwift.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 35F8386329E52B5600CC2335 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 35F8386529E52B5600CC2335 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 35F8386729E52B5600CC2335 /* ProductsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsViewController.swift; sourceTree = ""; }; - 35F8386A29E52B5600CC2335 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 35F8386C29E52B5D00CC2335 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 35F8386F29E52B5D00CC2335 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 35F8387129E52B5D00CC2335 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 35F8388029E52DCE00CC2335 /* Napier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Napier.swift; sourceTree = ""; }; - 35F8388229E52DE800CC2335 /* DIContainer+get.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DIContainer+get.swift"; sourceTree = ""; }; - 35F8388629E52E1300CC2335 /* kotlinxCoroutinesFlow+RxSwift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "kotlinxCoroutinesFlow+RxSwift.swift"; sourceTree = ""; }; - 35F8388829E5375300CC2335 /* IosProductsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosProductsViewModel.swift; sourceTree = ""; }; - 66DAC5008E22112AA0B77211 /* 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 = ""; }; - 74CD297B2E427860BDD06623 /* Pods-iosApp-RxSwift.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp-RxSwift.release.xcconfig"; path = "Target Support Files/Pods-iosApp-RxSwift/Pods-iosApp-RxSwift.release.xcconfig"; sourceTree = ""; }; - 7555FF7B242A565900829871 /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 7555FF82242A565900829871 /* ProductsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsView.swift; sourceTree = ""; }; - 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 9A843F298B8B7BBF27ADB620 /* Pods_iosApp_RxSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp_RxSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - E46F456417F73A63CD104B21 /* 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 */ - 35F8385E29E52B5600CC2335 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - F7C477ABF007F86120032AB8 /* Pods_iosApp_RxSwift.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - C0ACCB18154326F13A615831 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 64DE2C15350EA835D95C1039 /* 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 = ""; - }; - 135C2B7FC9170CA073CAAA3E /* Frameworks */ = { - isa = PBXGroup; - children = ( - 2E8E8ABD96E9CB70C7023D30 /* Pods_iosApp.framework */, - 9A843F298B8B7BBF27ADB620 /* Pods_iosApp_RxSwift.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - 352D59F52998F130008142AF /* Utils */ = { - isa = PBXGroup; - children = ( - 352D59EF2998EDD3008142AF /* kotlinxCoroutinesFlowExtensions.swift */, - 352D59F12998EE6C008142AF /* Napier.swift */, - 352D59F62998F1A4008142AF /* DIContainer+get.swift */, - 35F5E36D29B3B6EB00EE8FF8 /* String+toDate.swift */, - ); - path = Utils; - sourceTree = ""; - }; - 354544A629BA2C58003FE581 /* Snippets */ = { - isa = PBXGroup; - children = ( - 354544A729BA2C6B003FE581 /* SnippetsView.swift */, - ); - path = Snippets; - sourceTree = ""; - }; - 35C522C929E6643900606C46 /* Products */ = { - isa = PBXGroup; - children = ( - 35F8386729E52B5600CC2335 /* ProductsViewController.swift */, - 35F8388829E5375300CC2335 /* IosProductsViewModel.swift */, - ); - path = Products; - sourceTree = ""; - }; - 35E34EBF29BDE01900ED7D75 /* Others */ = { - isa = PBXGroup; - children = ( - 35E34EBD29BCDDD300ED7D75 /* async_let.swift */, - 35E34EC029BDE02F00ED7D75 /* callback_to_async.swift */, - ); - path = Others; - sourceTree = ""; - }; - 35F5E36029AA17FD00EE8FF8 /* Products */ = { - isa = PBXGroup; - children = ( - 352D59F32998F119008142AF /* IosProductsViewModel.swift */, - 7555FF82242A565900829871 /* ProductsView.swift */, - ); - path = Products; - sourceTree = ""; - }; - 35F5E36129AA18D600EE8FF8 /* Search Products */ = { - isa = PBXGroup; - children = ( - 35F5E36229AA18EC00EE8FF8 /* SearchProductsView.swift */, - 35F5E36629AA19D700EE8FF8 /* IosSearchProductsViewModel.swift */, - ); - path = "Search Products"; - sourceTree = ""; - }; - 35F5E36829B3979700EE8FF8 /* Product Detail */ = { - isa = PBXGroup; - children = ( - 35F5E36929B397B000EE8FF8 /* ProductDetailView.swift */, - 35F5E36B29B3996500EE8FF8 /* IosProductDetailViewModel.swift */, - ); - path = "Product Detail"; - sourceTree = ""; - }; - 35F8386229E52B5600CC2335 /* iosApp-RxSwift */ = { - isa = PBXGroup; - children = ( - 35C522C929E6643900606C46 /* Products */, - 35F8387F29E52DB000CC2335 /* Utils */, - 35F8386329E52B5600CC2335 /* AppDelegate.swift */, - 35F8386529E52B5600CC2335 /* SceneDelegate.swift */, - 35F8386929E52B5600CC2335 /* Main.storyboard */, - 35F8386C29E52B5D00CC2335 /* Assets.xcassets */, - 35F8386E29E52B5D00CC2335 /* LaunchScreen.storyboard */, - 35F8387129E52B5D00CC2335 /* Info.plist */, - ); - path = "iosApp-RxSwift"; - sourceTree = ""; - }; - 35F8387F29E52DB000CC2335 /* Utils */ = { - isa = PBXGroup; - children = ( - 35F8388029E52DCE00CC2335 /* Napier.swift */, - 35F8388229E52DE800CC2335 /* DIContainer+get.swift */, - 35F8388629E52E1300CC2335 /* kotlinxCoroutinesFlow+RxSwift.swift */, - ); - path = Utils; - sourceTree = ""; - }; - 7555FF72242A565900829871 = { - isa = PBXGroup; - children = ( - 7555FF7D242A565900829871 /* iosApp */, - 35F8386229E52B5600CC2335 /* iosApp-RxSwift */, - 7555FF7C242A565900829871 /* Products */, - DE7A2414E7A5C2A1C33289FF /* Pods */, - 135C2B7FC9170CA073CAAA3E /* Frameworks */, - ); - sourceTree = ""; - }; - 7555FF7C242A565900829871 /* Products */ = { - isa = PBXGroup; - children = ( - 7555FF7B242A565900829871 /* iosApp.app */, - 35F8386129E52B5600CC2335 /* iosApp-RxSwift.app */, - ); - name = Products; - sourceTree = ""; - }; - 7555FF7D242A565900829871 /* iosApp */ = { - isa = PBXGroup; - children = ( - 35E34EBF29BDE01900ED7D75 /* Others */, - 354544A629BA2C58003FE581 /* Snippets */, - 35F5E36829B3979700EE8FF8 /* Product Detail */, - 35F5E36129AA18D600EE8FF8 /* Search Products */, - 35F5E36029AA17FD00EE8FF8 /* Products */, - 352D59F52998F130008142AF /* Utils */, - 058557BA273AAA24004C7B11 /* Assets.xcassets */, - 7555FF8C242A565B00829871 /* Info.plist */, - 2152FB032600AC8F00CF470E /* iOSApp.swift */, - 058557D7273AAEEB004C7B11 /* Preview Content */, - 35F5E36429AA191F00EE8FF8 /* CommonUi.swift */, - ); - path = iosApp; - sourceTree = ""; - }; - DE7A2414E7A5C2A1C33289FF /* Pods */ = { - isa = PBXGroup; - children = ( - E46F456417F73A63CD104B21 /* Pods-iosApp.debug.xcconfig */, - 66DAC5008E22112AA0B77211 /* Pods-iosApp.release.xcconfig */, - 320FC384F13C505E6AB66632 /* Pods-iosApp-RxSwift.debug.xcconfig */, - 74CD297B2E427860BDD06623 /* Pods-iosApp-RxSwift.release.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 35F8386029E52B5600CC2335 /* iosApp-RxSwift */ = { - isa = PBXNativeTarget; - buildConfigurationList = 35F8387429E52B5D00CC2335 /* Build configuration list for PBXNativeTarget "iosApp-RxSwift" */; - buildPhases = ( - B656F0DD010A3D3616177C06 /* [CP] Check Pods Manifest.lock */, - 35F8385D29E52B5600CC2335 /* Sources */, - 35F8385E29E52B5600CC2335 /* Frameworks */, - 35F8385F29E52B5600CC2335 /* Resources */, - 77936D1C83531C094C448405 /* [CP] Embed Pods Frameworks */, - 8697E55BC90D1FE214337624 /* [CP] Copy Pods Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "iosApp-RxSwift"; - productName = "iosApp-RxSwift"; - productReference = 35F8386129E52B5600CC2335 /* iosApp-RxSwift.app */; - productType = "com.apple.product-type.application"; - }; - 7555FF7A242A565900829871 /* iosApp */ = { - isa = PBXNativeTarget; - buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */; - buildPhases = ( - 7ACBDDA715718C6D9882FF30 /* [CP] Check Pods Manifest.lock */, - 7555FF77242A565900829871 /* Sources */, - 7555FF79242A565900829871 /* Resources */, - C0ACCB18154326F13A615831 /* Frameworks */, - 9B2B5D25329D0690C66216E5 /* [CP] Embed Pods Frameworks */, - 6F7CE651A23EFEFAE1DA3EB8 /* [CP] Copy Pods Resources */, - ); - 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 = 1320; - LastUpgradeCheck = 1130; - ORGANIZATIONNAME = orgName; - TargetAttributes = { - 35F8386029E52B5600CC2335 = { - CreatedOnToolsVersion = 13.2.1; - }; - 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 */, - 35F8386029E52B5600CC2335 /* iosApp-RxSwift */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 35F8385F29E52B5600CC2335 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 35F8387029E52B5D00CC2335 /* LaunchScreen.storyboard in Resources */, - 35F8386D29E52B5D00CC2335 /* Assets.xcassets in Resources */, - 35F8386B29E52B5600CC2335 /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 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 */ - 6F7CE651A23EFEFAE1DA3EB8 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - 77936D1C83531C094C448405 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-iosApp-RxSwift/Pods-iosApp-RxSwift-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-iosApp-RxSwift/Pods-iosApp-RxSwift-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp-RxSwift/Pods-iosApp-RxSwift-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 7ACBDDA715718C6D9882FF30 /* [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; - }; - 8697E55BC90D1FE214337624 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-iosApp-RxSwift/Pods-iosApp-RxSwift-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-iosApp-RxSwift/Pods-iosApp-RxSwift-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp-RxSwift/Pods-iosApp-RxSwift-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - 9B2B5D25329D0690C66216E5 /* [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; - }; - B656F0DD010A3D3616177C06 /* [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-RxSwift-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 */ - 35F8385D29E52B5600CC2335 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 35F8388929E5375300CC2335 /* IosProductsViewModel.swift in Sources */, - 35F8386829E52B5600CC2335 /* ProductsViewController.swift in Sources */, - 35F8388729E52E1300CC2335 /* kotlinxCoroutinesFlow+RxSwift.swift in Sources */, - 35F8386429E52B5600CC2335 /* AppDelegate.swift in Sources */, - 35F8388329E52DE800CC2335 /* DIContainer+get.swift in Sources */, - 35F8388129E52DCE00CC2335 /* Napier.swift in Sources */, - 35F8386629E52B5600CC2335 /* SceneDelegate.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 7555FF77242A565900829871 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 35F5E36729AA19D700EE8FF8 /* IosSearchProductsViewModel.swift in Sources */, - 35F5E36E29B3B6EB00EE8FF8 /* String+toDate.swift in Sources */, - 354544A829BA2C6B003FE581 /* SnippetsView.swift in Sources */, - 35F5E36A29B397B000EE8FF8 /* ProductDetailView.swift in Sources */, - 35F5E36529AA191F00EE8FF8 /* CommonUi.swift in Sources */, - 35F5E36329AA18EC00EE8FF8 /* SearchProductsView.swift in Sources */, - 35E34EBE29BCDDD300ED7D75 /* async_let.swift in Sources */, - 352D59F02998EDD3008142AF /* kotlinxCoroutinesFlowExtensions.swift in Sources */, - 35E34EC129BDE02F00ED7D75 /* callback_to_async.swift in Sources */, - 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, - 352D59F22998EE6C008142AF /* Napier.swift in Sources */, - 352D59F72998F1A4008142AF /* DIContainer+get.swift in Sources */, - 352D59F42998F119008142AF /* IosProductsViewModel.swift in Sources */, - 35F5E36C29B3996500EE8FF8 /* IosProductDetailViewModel.swift in Sources */, - 7555FF83242A565900829871 /* ProductsView.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 35F8386929E52B5600CC2335 /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 35F8386A29E52B5600CC2335 /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 35F8386E29E52B5D00CC2335 /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 35F8386F29E52B5D00CC2335 /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 35F8387229E52B5D00CC2335 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 320FC384F13C505E6AB66632 /* Pods-iosApp-RxSwift.debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = dwarf; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "iosApp-RxSwift/Info.plist"; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.hoc081098.iosApp-RxSwift"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 35F8387329E52B5D00CC2335 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 74CD297B2E427860BDD06623 /* Pods-iosApp-RxSwift.release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "iosApp-RxSwift/Info.plist"; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.hoc081098.iosApp-RxSwift"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 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 = E46F456417F73A63CD104B21 /* Pods-iosApp.debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - 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 = 66DAC5008E22112AA0B77211 /* Pods-iosApp.release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - 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 */ - 35F8387429E52B5D00CC2335 /* Build configuration list for PBXNativeTarget "iosApp-RxSwift" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 35F8387229E52B5D00CC2335 /* Debug */, - 35F8387329E52B5D00CC2335 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 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/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme b/sample/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme deleted file mode 100644 index 5b29c0ae..00000000 --- a/sample/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/sample/iosApp/iosApp.xcworkspace/contents.xcworkspacedata b/sample/iosApp/iosApp.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index c009e7d7..00000000 --- a/sample/iosApp/iosApp.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/sample/iosApp/iosApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/sample/iosApp/iosApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/sample/iosApp/iosApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/sample/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/sample/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index ee7e3ca0..00000000 --- a/sample/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} \ No newline at end of file diff --git a/sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index fb88a396..00000000 --- a/sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "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/iosApp/iosApp/Assets.xcassets/Contents.json b/sample/iosApp/iosApp/Assets.xcassets/Contents.json deleted file mode 100644 index 4aa7c535..00000000 --- a/sample/iosApp/iosApp/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} \ No newline at end of file diff --git a/sample/iosApp/iosApp/CommonUi.swift b/sample/iosApp/iosApp/CommonUi.swift deleted file mode 100644 index 4ba0162a..00000000 --- a/sample/iosApp/iosApp/CommonUi.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// CommonUI.swift -// iosApp -// -// Created by Petrus Nguyen Thai Hoc on 2/25/23. -// Copyright © 2023 orgName. All rights reserved. -// - -import Foundation -import SwiftUI -import shared -import Kingfisher - -struct ProductItemRow: View { - private let item: ProductItemUi - private let url: URL? - - init(item: ProductItemUi) { - self.item = item - self.url = self.item.images.first.flatMap(URL.init(string:)) - } - - var body: some View { - HStack { - KFAnimatedImage(url) - .configure { view in view.framePreloadCount = 3 } - .cacheOriginalImage() - .onFailure { e in Napier.e(error: e, "err: url=\(String(describing: url)), e=\(e)") } - .placeholder { p in ProgressView(p) } - .fade(duration: 1) - .forceTransition() - .aspectRatio(contentMode: .fill) - .frame(width: 72, height: 72) - .cornerRadius(20) - .shadow(radius: 5) - .frame(width: 92, height: 92) - - VStack(alignment: .leading) { - Text(item.title) - .font(.headline) - .lineLimit(2) - .truncationMode(.tail) - - Spacer().frame(height: 10) - - Text(item.description_) - .font(.subheadline) - .lineLimit(2) - .truncationMode(.tail) - - Spacer().frame(height: 10) - }.padding([.bottom, .trailing, .top]) - } - } -} - -struct EmptyProductsView: View { - var body: some View { - VStack(alignment: .center) { - Text("Products is empty") - .font(.title3) - .multilineTextAlignment(.center) - .padding(10) - }.frame(maxWidth: .infinity) - } -} - -struct EmptyProductsView_Previews: PreviewProvider { - static var previews: some View { - EmptyProductsView() - } -} - -struct ErrorView: View { - let error: KotlinThrowable - let onRetry: () -> Void - - var body: some View { - VStack(alignment: .center) { - Text("Error: \(error.message ?? "unknown")") - .font(.title3) - .multilineTextAlignment(.center) - .padding(10) - - Button("Retry") { onRetry() } - }.frame(maxWidth: .infinity) - } -} diff --git a/sample/iosApp/iosApp/Info.plist b/sample/iosApp/iosApp/Info.plist deleted file mode 100644 index 8044709c..00000000 --- a/sample/iosApp/iosApp/Info.plist +++ /dev/null @@ -1,48 +0,0 @@ - - - - - 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/iosApp/iosApp/Others/async_let.swift b/sample/iosApp/iosApp/Others/async_let.swift deleted file mode 100644 index 6a1543b4..00000000 --- a/sample/iosApp/iosApp/Others/async_let.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// async_let.swift -// iosApp -// -// Created by Petrus Nguyen Thai Hoc on 3/11/23. -// Copyright © 2023 orgName. All rights reserved. -// - -import Foundation - -private func getData1() async throws -> Int { - try await Task.sleep(nanoseconds: 2_000_000_000) - return 1 -} - -private func getData2() async throws -> Int { - try await Task.sleep(nanoseconds: 2_000_000_000) - return 2 -} - -private func test() { - Task { - async let i1 = getData1() - async let i2 = getData2() - let sum = try await (i1 + i2) - print(sum) - } -} - -private func testGroup() { - Task { - let results = try await withThrowingTaskGroup(of: Int.self, returning: [Int].self) { group in - group.addTask { try await getData1() } - group.addTask { try await getData2() } - - return try await group.reduce(into: [Int]()) { $0.append($1) } - } - print(results) - } -} diff --git a/sample/iosApp/iosApp/Others/callback_to_async.swift b/sample/iosApp/iosApp/Others/callback_to_async.swift deleted file mode 100644 index ce7d1446..00000000 --- a/sample/iosApp/iosApp/Others/callback_to_async.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// callback_to_async.swift -// iosApp -// -// Created by Petrus Nguyen Thai Hoc on 3/12/23. -// Copyright © 2023 orgName. All rights reserved. -// - -import Foundation - -private class DemoError: Error { } - -private final class GetDataRequest: @unchecked Sendable { - private let lock = NSLock() - private var item: DispatchWorkItem? - private var onCancel: (@Sendable () -> Void)? - - func start( - block: @Sendable @escaping (Result) -> Void, - onCancel: @Sendable @escaping () -> Void - ) { - self.lock.lock() - defer { self.lock.unlock() } - print("Started") - - self.item = .init { - block( - Bool.random() - ? .success(.random(in: 0...1_000)) - : .failure(DemoError()) - ) - } - self.onCancel = onCancel - - DispatchQueue - .global(qos: .userInitiated) - .asyncAfter(deadline: .now() + 2, execute: self.item!) - } - - func cancel() { - self.lock.lock() - defer { self.lock.unlock() } - - item?.cancel() - item = nil - - onCancel?() - onCancel = nil - print("Cancelled") - } -} - -private func getData() async throws -> Int { - let req = GetDataRequest() - - return try await withTaskCancellationHandler( - operation: { - // This check is necessary in case this code runs after the task was cancelled. - // In which case we want to bail right away. - try Task.checkCancellation() - - return try await withCheckedThrowingContinuation { cont in - req.start( - block: { - // This check is necessary in case this code runs after the task was cancelled. - // In which case we want to bail right away. - guard !Task.isCancelled else { return cont.resume(throwing: CancellationError()) } - - cont.resume(with: $0) - }, - onCancel: { - cont.resume(throwing: CancellationError()) - } - ) - } - }, - onCancel: { req.cancel() } - ) -} - -private func use() { - let task = Task.detached { - do { print("Result:", try await getData()) } - catch { print("Error:", error) } - } - DispatchQueue.main.asyncAfter(deadline: .now() + 10) { - print("Start cancelling") - task.cancel() - } -} diff --git a/sample/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/sample/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 4aa7c535..00000000 --- a/sample/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} \ No newline at end of file diff --git a/sample/iosApp/iosApp/Product Detail/IosProductDetailViewModel.swift b/sample/iosApp/iosApp/Product Detail/IosProductDetailViewModel.swift deleted file mode 100644 index 686134ac..00000000 --- a/sample/iosApp/iosApp/Product Detail/IosProductDetailViewModel.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// IosProductDetailViewModel.swift -// iosApp -// -// Created by Petrus Nguyen Thai Hoc on 3/4/23. -// Copyright © 2023 orgName. All rights reserved. -// - -import Foundation -import shared -import Combine - -@MainActor -class IosProductDetailViewModel: ObservableObject { - private let commonVm: ProductDetailViewModel - - @Published var state: ProductDetailState - - init(id: Int32) { - self.commonVm = DIContainer.shared.get( - parameters: { - let parameters: [Any] = [id] - return DIContainer.shared.parametersOf(parameters: parameters) - } - ) - - self.state = self.commonVm.stateFlow.value - self.commonVm.stateFlow.subscribe( - scope: self.commonVm.viewModelScope, - onValue: { [weak self] in self?.state = $0 } - ) - } - - func refresh() { self.commonVm.refresh() } - - func retry() { self.commonVm.retry() } - - deinit { - self.commonVm.clear() - Napier.d("\(self)::deinit") - } -} diff --git a/sample/iosApp/iosApp/Product Detail/ProductDetailView.swift b/sample/iosApp/iosApp/Product Detail/ProductDetailView.swift deleted file mode 100644 index d37d71c2..00000000 --- a/sample/iosApp/iosApp/Product Detail/ProductDetailView.swift +++ /dev/null @@ -1,144 +0,0 @@ -// -// ProductDetailView.swift -// iosApp -// -// Created by Petrus Nguyen Thai Hoc on 3/4/23. -// Copyright © 2023 orgName. All rights reserved. -// - -import SwiftUI -import shared -import Kingfisher - -struct ProductDetailView: View { - @Environment(\.scenePhase) var scenePhase - - @ObservedObject var viewModel: IosProductDetailViewModel - - init(id: Int32) { - self.viewModel = IosProductDetailViewModel(id: id) - } - - var body: some View { - let state = self.viewModel.state - - return ZStack(alignment: .center) { - ZStack(alignment: .center) { - if state is ProductDetailStateLoading { - ProgressView("Loading...") - } - if let state = state as? ProductDetailStateError { - ErrorView( - error: state.error, - onRetry: { self.viewModel.retry() } - ) - } - if let state = state as? ProductDetailStateSuccess { - DetailContentView(product: state.product) - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .navigationTitle("Product detail") - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .onAppear { self.viewModel.refresh() } - .onChange(of: scenePhase) { newPhase in - if case .active = newPhase { - self.viewModel.refresh() - } - } - } -} - -struct ProductDetailView_Previews: PreviewProvider { - static var previews: some View { - ProductDetailView(id: 0) - } -} - - -private struct DetailContentView: View { - let product: ProductItemUi - - private let tuples: [(String, String)] - - init(product: ProductItemUi) { - self.product = product - - self.tuples = [ - ("Title: ", product.title), - ("Price: ", String(product.price)), - ("Description: ", product.description_), - ("Category: ", product.category.name), - ("Created at: ", product.creationAt.toDate(.isoDateTimeMilliSec)?.toLongString ?? "unknown"), - ("Updated at: ", product.updatedAt.toDate(.isoDateTimeMilliSec)?.toLongString ?? "unknown"), - ] - } - - var body: some View { - ScrollView { - VStack { - KFAnimatedImage(product.images.first.flatMap(URL.init(string:))) - .configure { view in view.framePreloadCount = 3 } - .cacheOriginalImage() - .onFailure { e in - Napier.e( - error: e, - "err: url=\(String(describing: product.images.first)), e=\(e)" - ) - } - .placeholder { p in ProgressView(p) } - .fade(duration: 1) - .forceTransition() - .aspectRatio(1, contentMode: .fill) - .frame( - width: UIScreen.main.bounds.size.width - 96, - height: UIScreen.main.bounds.size.width - 96 - ) - .cornerRadius(12) - .shadow(radius: 5) - - Spacer().frame(height: 32) - - ForEach(tuples, id: \.0) { - SimpleTile(pair: $0) - - Spacer().frame(height: 8) - } - }.frame(maxWidth: .infinity) - .padding() - } - } -} - -private struct SimpleTile: View { - let pair: (String, String) - - var body: some View { - HStack(alignment: .top) { - Text(pair.0) - .font(.title3) - .bold() - .multilineTextAlignment(.leading) - - Spacer().frame(width: 8) - - Text(pair.1) - .font(.body) - .frame(maxWidth: .infinity) - .multilineTextAlignment(.trailing) - }.frame(maxWidth: .infinity) - } -} - -extension Date { - static let longFormatter: DateFormatter = { - let f = DateFormatter() - f.dateStyle = .long - return f - }() - - var toLongString: String { - Self.longFormatter.string(from: self) - } -} diff --git a/sample/iosApp/iosApp/Products/IosProductsViewModel.swift b/sample/iosApp/iosApp/Products/IosProductsViewModel.swift deleted file mode 100644 index a50461c0..00000000 --- a/sample/iosApp/iosApp/Products/IosProductsViewModel.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// IosProductsViewModel.swift -// iosApp -// -// Created by Petrus Nguyen Thai Hoc on 2/12/23. -// Copyright © 2023 orgName. All rights reserved. -// - -import Foundation -import shared -import Combine - -@MainActor -class IosProductsViewModel: ObservableObject { - private let commonVm: ProductsViewModel = DIContainer.shared.get() - - @Published private(set) var state: ProductsState - - let singleEventPublisher: AnyPublisher - - init() { - self.singleEventPublisher = self.commonVm.eventFlow - .asNonNullPublisher( - ProductSingleEvent.self, - dispatcher: DIContainer.shared - .get(for: AppDispatchers.self) - .immediateMain - ) - .handleEvents(receiveCancel: { Napier.d("eventFlow cancelled") }) - .assertNoFailure() - .eraseToAnyPublisher() - - self.state = self.commonVm.stateFlow.value - self.commonVm.stateFlow.subscribe( - scope: self.commonVm.viewModelScope, - onValue: { [weak self] in self?.state = $0 } - ) - } - - func dispatch(action: ProductsAction) { - self.commonVm.dispatch(action: action) - } - - deinit { - self.commonVm.clear() - Napier.d("\(self)::deinit") - } - - // MARK: Demo purpose only - private var cancellable: AnyCancellable? = nil - - private lazy var tickerPublisher: AnyPublisher = self.commonVm - .tickerFlow - .asNullablePublisher( - NSString.self, - dispatcher: DIContainer.shared - .get(for: AppDispatchers.self) - .immediateMain - ) - .map { $0 as String? } - .assertNoFailure() - .eraseToAnyPublisher() - - func onActive() { - self.cancellable?.cancel() - self.cancellable = self - .tickerPublisher - .sink( - receiveCompletion: { completion in - switch completion { - case .finished: - Napier.d("[ticker]: finished") - case .failure(let error): - Napier.e(error: error, "[ticker]: failure") - } - }, - receiveValue: { v in Napier.d("[ticker]: value=\(v ?? "nil")") } - ) - } - - func onInactive() { - self.cancellable?.cancel() - self.cancellable = nil - } -} diff --git a/sample/iosApp/iosApp/Products/ProductsView.swift b/sample/iosApp/iosApp/Products/ProductsView.swift deleted file mode 100644 index d0acebdc..00000000 --- a/sample/iosApp/iosApp/Products/ProductsView.swift +++ /dev/null @@ -1,81 +0,0 @@ -import SwiftUI -import shared -import Kingfisher -import Combine - -struct ProductsView: View { - @Environment(\.scenePhase) var scenePhase - - @ObservedObject var viewModel = IosProductsViewModel() - - var body: some View { - let state = self.viewModel.state - - return VStack { - ZStack(alignment: .center) { - if state.isLoading { - ProgressView("Loading...") - } - else if let error = state.error { - ErrorView( - error: error, - onRetry: { self.viewModel.dispatch(action: ProductsActionLoad()) } - ) - } - else if state.products.isEmpty { - EmptyProductsView() - } else { - List { - ForEach(state.products, id: \.id) { item in - NavigationLink(destination: LazyView(ProductDetailView(id: item.id))) { - ProductItemRow(item: item) - } - } - }.refreshable { - self.viewModel.dispatch(action: ProductsActionRefresh()) - - try? await Task.sleep(nanoseconds: 1_000_000) // 1ms - - // await the first state where isRefreshing is false. - let _: ProductsState? = await self.viewModel - .$state - .first(where: { !$0.isRefreshing }) - .values - .first(where: { _ in true }) - } - } - }.frame(maxHeight: .infinity) - .navigationTitle("Products") - }.onAppear { - if !state.hasContent { - self.viewModel.dispatch(action: ProductsActionLoad()) - } - self.viewModel.onActive() - } - .onDisappear { - self.viewModel.onInactive() - } - .onChange(of: scenePhase) { newPhase in - switch newPhase { - case .inactive: - self.viewModel.onInactive() - case .active: - self.viewModel.onActive() - case .background: - () - @unknown default: - fatalError() - } - } - .onReceive(self.viewModel.singleEventPublisher) { event in - Napier.d("[ProductsView] received \(event)") - } - } -} - - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ProductsView() - } -} diff --git a/sample/iosApp/iosApp/Search Products/IosSearchProductsViewModel.swift b/sample/iosApp/iosApp/Search Products/IosSearchProductsViewModel.swift deleted file mode 100644 index a691b7de..00000000 --- a/sample/iosApp/iosApp/Search Products/IosSearchProductsViewModel.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// IosSearchProductsViewModel.swift -// iosApp -// -// Created by Petrus Nguyen Thai Hoc on 2/25/23. -// Copyright © 2023 orgName. All rights reserved. -// - -import Foundation - - -import Foundation -import shared -import Combine - -@MainActor -class IosSearchProductsViewModel: ObservableObject { - private let commonVm: SearchProductsViewModel = DIContainer.shared.get() - - @Published private(set) var state: SearchProductsState - @Published private(set) var term: String = "" - - init() { - self.state = self.commonVm.stateFlow.value - self.commonVm.stateFlow.subscribe( - scope: self.commonVm.viewModelScope, - onValue: { [weak self] in self?.state = $0 } - ) - - self.commonVm - .searchTermStateFlow - .asNonNullPublisher( - NSString.self, - dispatcher: DIContainer.shared - .get(for: AppDispatchers.self) - .immediateMain - ) - .handleEvents(receiveCancel: { Napier.d("searchTermStateFlow cancelled") }) - .assertNoFailure() - .map { $0 as String } - .assign(to: &$term) - } - - func search(term: String) { - self.commonVm.search(term: term) - } - - deinit { - self.commonVm.clear() - Napier.d("\(self)::deinit") - } -} diff --git a/sample/iosApp/iosApp/Search Products/SearchProductsView.swift b/sample/iosApp/iosApp/Search Products/SearchProductsView.swift deleted file mode 100644 index 4b001153..00000000 --- a/sample/iosApp/iosApp/Search Products/SearchProductsView.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// SearchProductsView.swift -// iosApp -// -// Created by Petrus Nguyen Thai Hoc on 2/25/23. -// Copyright © 2023 orgName. All rights reserved. -// - -import SwiftUI -import shared - -struct SearchProductsView: View { - @ObservedObject var viewModel = IosSearchProductsViewModel() - - var body: some View { - let state = self.viewModel.state - - let termBinding = Binding( - get: { self.viewModel.term }, - set: { self.viewModel.search(term: $0) } - ) - - return VStack { - TextField( - "Search term", - text: termBinding - ).padding() - - ZStack(alignment: .center) { - if state.isLoading { - ProgressView("Loading...") - } - else if let error = state.error { - ErrorView( - error: error, - onRetry: { } - ) - } - else if state.products.isEmpty { - EmptyProductsView() - } else { - List { - ForEach(state.products, id: \.id) { item in - NavigationLink(destination: LazyView(ProductDetailView(id: item.id))) { - ProductItemRow(item: item) - } - } - } - } - }.frame(maxHeight: .infinity) - .navigationTitle("Search products") - }.frame(maxWidth: .infinity, maxHeight: .infinity) - } -} - -struct SearchProductsView_Previews: PreviewProvider { - static var previews: some View { - SearchProductsView() - } -} diff --git a/sample/iosApp/iosApp/Snippets/SnippetsView.swift b/sample/iosApp/iosApp/Snippets/SnippetsView.swift deleted file mode 100644 index 6305e9fe..00000000 --- a/sample/iosApp/iosApp/Snippets/SnippetsView.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// IosUserViewModel.swift -// iosApp -// -// Created by Petrus Nguyen Thai Hoc on 3/9/23. -// Copyright © 2023 orgName. All rights reserved. -// - -import Foundation -import shared -import Combine -import SwiftUI - -private actor FakeGetUserUseCaseActor { - private var count = 0 - - func call() async throws -> User? { - try await Task.sleep(nanoseconds: 1 * 1_000_000_000) - - self.count += 1 - if self.count.isMultiple(of: 2) { - return nil - } else { - return User(id: Int64(count), name: "hoc081098") - } - } -} - -private class FakeGetUserUseCase: KotlinSuspendFunction0 { - private let actor = FakeGetUserUseCaseActor() - - func invoke() async throws -> Any? { try await self.`actor`.call() } -} - -@MainActor -class IosUserViewModel: ObservableObject { - private let commonVm: UserViewModel = UserViewModel.init( - savedStateHandle: .init(), - getUserUseCase: FakeGetUserUseCase() - ) - - @Published private(set) var user: User? - - init() { - self.commonVm.userStateFlow.subscribe( - scope: self.commonVm.viewModelScope, - onValue: { [weak self] in self?.user = $0 } - ) - } - - func getUser() { self.commonVm.getUser() } - - deinit { - self.commonVm.clear() - Napier.d("\(self)::deinit") - } -} - -struct SnippetsView: View { - @ObservedObject var viewModel = IosUserViewModel() - - private let timerPublisher = Timer - .publish(every: 2, on: .main, in: .common) - .autoconnect() - - var body: some View { - Text("User: \(viewModel.user?.description() ?? "nil")") - .onReceive(timerPublisher) { _ in viewModel.getUser() } - } -} - -struct SnippetsView_Previews: PreviewProvider { - static var previews: some View { - SnippetsView() - } -} diff --git a/sample/iosApp/iosApp/Utils/DIContainer+get.swift b/sample/iosApp/iosApp/Utils/DIContainer+get.swift deleted file mode 100644 index 919f04b8..00000000 --- a/sample/iosApp/iosApp/Utils/DIContainer+get.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// DIContainer+get.swift -// iosApp -// -// Created by Petrus Nguyen Thai Hoc on 2/12/23. -// Copyright © 2023 orgName. All rights reserved. -// - -import Foundation -import shared - -extension DIContainer { - func get( - for type: T.Type = T.self, - qualifier: Koin_coreQualifier? = nil, - parameters: (() -> Koin_coreParametersHolder)? = nil - ) -> T { - self.get( - type: type, - qualifier: qualifier, - parameters: parameters - ) as! T - } -} diff --git a/sample/iosApp/iosApp/Utils/Napier.swift b/sample/iosApp/iosApp/Utils/Napier.swift deleted file mode 100644 index ebeb3cb3..00000000 --- a/sample/iosApp/iosApp/Utils/Napier.swift +++ /dev/null @@ -1,152 +0,0 @@ -// -// Napier.swift -// iosApp -// -// Created by Petrus Nguyen Thai Hoc on 2/12/23. -// Copyright © 2023 orgName. All rights reserved. -// - -import Foundation -import shared - -extension Napier { - static func v( - tag: String? = nil, - error: Error? = nil, - _ items: Any..., - separator: String = " ", - file: String = #file, - function: String = #function - ) { - log( - logLevel: .verbose, - error: error, - tag: tag, - items, - separator: separator, - file: file, - function: function) - } - - static func d( - tag: String? = nil, - error: Error? = nil, - _ items: Any..., - separator: String = " ", - file: String = #file, - function: String = #function - ) { - log( - logLevel: .debug, - error: error, - tag: tag, - items, - separator: separator, - file: file, - function: function) - } - - static func i( - tag: String? = nil, - error: Error? = nil, - _ items: Any..., - separator: String = " ", - file: String = #file, - function: String = #function - ) { - log( - logLevel: .info, - error: error, - tag: tag, - items, - separator: separator, - file: file, - function: function) - } - - static func w( - tag: String? = nil, - error: Error? = nil, - _ items: Any..., - separator: String = " ", - file: String = #file, - function: String = #function - ) { - log( - logLevel: .warning, - error: error, - tag: tag, - items, - separator: separator, - file: file, - function: function) - } - - static func e( - tag: String? = nil, - error: Error? = nil, - _ items: Any..., - separator: String = " ", - file: String = #file, - function: String = #function - ) { - log( - logLevel: .error, - error: error, - tag: tag, - items, - separator: separator, - file: file, - function: function) - } - - static func a( - tag: String? = nil, - error: Error? = nil, - _ items: Any..., - separator: String = " ", - file: String = #file, - function: String = #function - ) { - log( - logLevel: .assert, - error: error, - tag: tag, - items, - separator: separator, - file: file, - function: function) - } - - static private func log( - logLevel: LogLevel, - error: Error?, - tag: String?, - _ items: [Any], - separator: String, - file: String, - function: String - ) { - let message = items.map { "\($0)" }.joined(separator: separator) - - let throwable: KotlinThrowable? - if let error = error { - throwable = NSErrorKt.asThrowable(error) - } else { - throwable = nil - } - - Self.shared.log( - priority: logLevel, - tag: tag ?? Self.defaultTag(file: file, function: function), - throwable: throwable, - message: message - ) - } - - private static func defaultTag(file: String, function: String) -> String { - let fileName = URL(fileURLWithPath: file).lastPathComponent - let functionName = String(function.prefix(while: { $0 != "(" })) - return "\(fileName):\(functionName)" - } -} diff --git a/sample/iosApp/iosApp/Utils/String+toDate.swift b/sample/iosApp/iosApp/Utils/String+toDate.swift deleted file mode 100644 index 95733040..00000000 --- a/sample/iosApp/iosApp/Utils/String+toDate.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// String+toDate.swift -// iosApp -// -// Created by Petrus Nguyen Thai Hoc on 3/5/23. -// Copyright © 2023 orgName. All rights reserved. -// - -import Foundation - -extension String { - public enum DateFormatType { - - /// The ISO8601 formatted year "yyyy" i.e. 1997 - case isoYear - - /// The ISO8601 formatted year and month "yyyy-MM" i.e. 1997-07 - case isoYearMonth - - /// The ISO8601 formatted date "yyyy-MM-dd" i.e. 1997-07-16 - case isoDate - - /// The ISO8601 formatted date and time "yyyy-MM-dd'T'HH:mmZ" i.e. 1997-07-16T19:20+01:00 - case isoDateTime - - /// The ISO8601 formatted date, time and sec "yyyy-MM-dd'T'HH:mm:ssZ" i.e. 1997-07-16T19:20:30+01:00 - case isoDateTimeSec - - /// The ISO8601 formatted date, time and millisec "yyyy-MM-dd'T'HH:mm:ss.SSSZ" i.e. 1997-07-16T19:20:30.45+01:00 - case isoDateTimeMilliSec - - /// The dotNet formatted date "/Date(%d%d)/" i.e. "/Date(1268123281843)/" - case dotNet - - /// The RSS formatted date "EEE, d MMM yyyy HH:mm:ss ZZZ" i.e. "Fri, 09 Sep 2011 15:26:08 +0200" - case rss - - /// The Alternative RSS formatted date "d MMM yyyy HH:mm:ss ZZZ" i.e. "09 Sep 2011 15:26:08 +0200" - case altRSS - - /// The http header formatted date "EEE, dd MM yyyy HH:mm:ss ZZZ" i.e. "Tue, 15 Nov 1994 12:45:26 GMT" - case httpHeader - - /// A generic standard format date i.e. "EEE MMM dd HH:mm:ss Z yyyy" - case standard - - /// A custom date format string - case custom(String) - - /// The local formatted date and time "yyyy-MM-dd HH:mm:ss" i.e. 1997-07-16 19:20:00 - case localDateTimeSec - - /// The local formatted date "yyyy-MM-dd" i.e. 1997-07-16 - case localDate - - /// The local formatted time "hh:mm a" i.e. 07:20 am - case localTimeWithNoon - - /// The local formatted date and time "yyyyMMddHHmmss" i.e. 19970716192000 - case localPhotoSave - - case birthDateFormatOne - - case birthDateFormatTwo - - /// - case messageRTetriveFormat - - /// - case emailTimePreview - - var stringFormat: String { - switch self { - //handle iso Time - case .birthDateFormatOne: return "dd/MM/YYYY" - case .birthDateFormatTwo: return "dd-MM-YYYY" - case .isoYear: return "yyyy" - case .isoYearMonth: return "yyyy-MM" - case .isoDate: return "yyyy-MM-dd" - case .isoDateTime: return "yyyy-MM-dd'T'HH:mmZ" - case .isoDateTimeSec: return "yyyy-MM-dd'T'HH:mm:ssZ" - case .isoDateTimeMilliSec: return "yyyy-MM-dd'T'HH:mm:ss.SSSZ" - case .dotNet: return "/Date(%d%f)/" - case .rss: return "EEE, d MMM yyyy HH:mm:ss ZZZ" - case .altRSS: return "d MMM yyyy HH:mm:ss ZZZ" - case .httpHeader: return "EEE, dd MM yyyy HH:mm:ss ZZZ" - case .standard: return "EEE MMM dd HH:mm:ss Z yyyy" - case .custom(let customFormat): return customFormat - - //handle local Time - case .localDateTimeSec: return "yyyy-MM-dd HH:mm:ss" - case .localTimeWithNoon: return "hh:mm a" - case .localDate: return "yyyy-MM-dd" - case .localPhotoSave: return "yyyyMMddHHmmss" - case .messageRTetriveFormat: return "yyyy-MM-dd'T'HH:mm:ssZZZZZ" - case .emailTimePreview: return "dd MMM yyyy, h:mm a" - } - } - } - - func toDate(_ format: DateFormatType = .isoDate) -> Date? { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = format.stringFormat - return dateFormatter.date(from: self) - } -} diff --git a/sample/iosApp/iosApp/Utils/kotlinxCoroutinesFlowExtensions.swift b/sample/iosApp/iosApp/Utils/kotlinxCoroutinesFlowExtensions.swift deleted file mode 100644 index a613a460..00000000 --- a/sample/iosApp/iosApp/Utils/kotlinxCoroutinesFlowExtensions.swift +++ /dev/null @@ -1,161 +0,0 @@ -// -// kotlinxCoroutinesFlowExtensions.swift -// iosApp -// -// Created by Petrus Nguyen Thai Hoc on 2/12/23. -// Copyright © 2023 orgName. All rights reserved. -// - -import Foundation -import shared -import Combine - -extension Flow { - // MARK: - Flow - - func asNonNullPublisher( - _ type: T.Type = T.self, - dispatcher: CoroutineDispatcher = Dispatchers.shared.Unconfined - ) -> AnyPublisher { - NonNullFlowPublisher(flow: self, dispatcher: dispatcher) - .eraseToAnyPublisher() - } - - // MARK: - Flow - - func asNullablePublisher( - _ type: T.Type = T.self, - dispatcher: CoroutineDispatcher = Dispatchers.shared.Unconfined - ) -> AnyPublisher { - NullableFlowPublisher(flow: self, dispatcher: dispatcher) - .eraseToAnyPublisher() - } -} - -private func supervisorScope(dispatcher: CoroutineDispatcher) -> CoroutineScope { - CoroutineScopeKt.CoroutineScope( - context: dispatcher.plus(context: SupervisorKt.SupervisorJob(parent: nil)) - ) -} - -// MARK: - NonNullFlowPublisher - -private struct NonNullFlowPublisher: Publisher { - typealias Output = T - typealias Failure = Error - - private let flow: Flow - private let dispatcher: CoroutineDispatcher - - init(flow: Flow, dispatcher: CoroutineDispatcher) { - self.flow = flow - self.dispatcher = dispatcher - } - - func receive(subscriber: S) where S: Subscriber, Error == S.Failure, T == S.Input { - let subscription = NonNullFlowSubscription( - flow: flow, - subscriber: subscriber, - dispatcher: dispatcher - ) - subscriber.receive(subscription: subscription) - } -} - -private class NonNullFlowSubscription: Subscription where S.Input == T, S.Failure == Error { - - private var subscriber: S? - private var closable: Closeable? - - init( - flow: Flow, - subscriber: S, - dispatcher: CoroutineDispatcher - ) { - self.subscriber = subscriber - - let wrapper = NonNullFlowWrapperKt.wrap(flow) as! NonNullFlowWrapper - self.closable = wrapper.subscribe( - scope: supervisorScope(dispatcher: dispatcher), - onValue: { - _ = subscriber.receive($0) - }, - onError: { - subscriber.receive(completion: .failure($0.asNSError())) - }, - onComplete: { - subscriber.receive(completion: .finished) - } - ) - } - - func request(_ demand: Subscribers.Demand) { } - - func cancel() { - self.subscriber = nil - - self.closable?.close() - self.closable = nil - } -} - -// MARK: - NullableFlowPublisher - -private struct NullableFlowPublisher: Publisher { - typealias Output = T? - typealias Failure = Error - - private let flow: Flow - private let dispatcher: CoroutineDispatcher - - init(flow: Flow, dispatcher: CoroutineDispatcher) { - self.flow = flow - self.dispatcher = dispatcher - } - - func receive(subscriber: S) where S: Subscriber, Error == S.Failure, T? == S.Input { - let subscription = NullableFlowSubscription( - flow: flow, - subscriber: subscriber, - dispatcher: dispatcher - ) - subscriber.receive(subscription: subscription) - } -} - -private class NullableFlowSubscription: Subscription where S.Input == T?, S.Failure == Error { - - private var subscriber: S? - private var closable: Closeable? - - init( - flow: Flow, - subscriber: S, - dispatcher: CoroutineDispatcher - ) { - self.subscriber = subscriber - - let wrapper = NullableFlowWrapperKt.wrap(flow) as! NullableFlowWrapper - self.closable = wrapper.subscribe( - scope: supervisorScope(dispatcher: dispatcher), - onValue: { - _ = subscriber.receive($0) - }, - onError: { - subscriber.receive(completion: .failure($0.asNSError())) - }, - onComplete: { - subscriber.receive(completion: .finished) - } - ) - } - - func request(_ demand: Subscribers.Demand) { } - - func cancel() { - self.subscriber = nil - - self.closable?.close() - self.closable = nil - } -} diff --git a/sample/iosApp/iosApp/iOSApp.swift b/sample/iosApp/iosApp/iOSApp.swift deleted file mode 100644 index 65d050ed..00000000 --- a/sample/iosApp/iosApp/iOSApp.swift +++ /dev/null @@ -1,42 +0,0 @@ -import SwiftUI -import shared - -@main -struct iOSApp: App { - init() { - DIContainer.shared.doInit { _ in } - } - - var body: some Scene { - WindowGroup { - NavigationView { - VStack(alignment: .center) { - NavigationLink("Products screen", destination: LazyView(ProductsView())) - - Spacer().frame(height: 32) - - NavigationLink("Search products screen", destination: LazyView(SearchProductsView())) - - Spacer().frame(height: 32) - - NavigationLink("Snippets", destination: LazyView(SnippetsView())) - } - .frame(maxWidth: .infinity) - .navigationTitle("KMP ViewModel sample") - .navigationBarTitleDisplayMode(.inline) - }.navigationViewStyle(.stack) - } - } -} - -struct LazyView: View { - let build: () -> Content - - init(_ build: @autoclosure @escaping () -> Content) { - self.build = build - } - - var body: Content { - build() - } -} diff --git a/sample/shared/build.gradle.kts b/sample/shared/build.gradle.kts index 20411fc1..307fa9b6 100644 --- a/sample/shared/build.gradle.kts +++ b/sample/shared/build.gradle.kts @@ -4,7 +4,6 @@ plugins { alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.jetbrains.compose) - alias(libs.plugins.kotlin.cocoapods) alias(libs.plugins.kotlin.serialization) @@ -44,23 +43,6 @@ kotlin { applyDefaultHierarchyTemplate() - cocoapods { - summary = "Some description for the Shared Module" - homepage = "Link to the Shared Module homepage" - version = "1.0" - ios.deploymentTarget = "14.1" - podfile = project.file("../iosApp/Podfile") - framework { - baseName = "shared" - - export(libs.kmp.viewmodel.core) - export(libs.kmp.viewmodel.savedstate) - - export(libs.napier) - export(libs.coroutines.core) - } - } - sourceSets { commonMain { dependencies { diff --git a/sample/shared/shared.podspec b/sample/shared/shared.podspec deleted file mode 100644 index 5cccd4e5..00000000 --- a/sample/shared/shared.podspec +++ /dev/null @@ -1,50 +0,0 @@ -Pod::Spec.new do |spec| - spec.name = 'shared' - spec.version = '1.0' - spec.homepage = 'Link to the Shared Module homepage' - spec.source = { :http=> ''} - spec.authors = '' - spec.license = '' - spec.summary = 'Some description for the Shared Module' - spec.vendored_frameworks = 'build/cocoapods/framework/shared.framework' - spec.libraries = 'c++' - spec.ios.deployment_target = '14.1' - - - if !Dir.exist?('build/cocoapods/framework/shared.framework') || Dir.empty?('build/cocoapods/framework/shared.framework') - raise " - - Kotlin framework 'shared' doesn't exist yet, so a proper Xcode project can't be generated. - 'pod install' should be executed after running ':generateDummyFramework' Gradle task: - - ./gradlew :sample:shared:generateDummyFramework - - Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)" - end - - spec.pod_target_xcconfig = { - 'KOTLIN_PROJECT_PATH' => ':sample:shared', - 'PRODUCT_MODULE_NAME' => 'shared', - } - - spec.script_phases = [ - { - :name => 'Build shared', - :execution_position => :before_compile, - :shell_path => '/bin/sh', - :script => <<-SCRIPT - if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then - echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\"" - exit 0 - fi - set -ev - REPO_ROOT="$PODS_TARGET_SRCROOT" - "$REPO_ROOT/../../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \ - -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \ - -Pkotlin.native.cocoapods.archs="$ARCHS" \ - -Pkotlin.native.cocoapods.configuration="$CONFIGURATION" - SCRIPT - } - ] - spec.resources = ['build/compose/ios/shared/compose-resources'] -end \ No newline at end of file From 21ec88857fcf3a1e0c719680c6957b232e4063d1 Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sun, 21 Jan 2024 16:35:12 +0700 Subject: [PATCH 27/28] basic done --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dff14d27..a469f0ad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,7 +67,6 @@ jobs: # ./gradlew :navigation-core:build --stacktrace - name: Build run: | - ./gradlew :khonshu-navigation-core:compileKotlinJvm :khonshu-navigation-core:compileDebugKotlinAndroid :khonshu-navigation-core:compileKotlinIosX64 --stacktrace ./gradlew :navigation:compileKotlinJvm :navigation:compileDebugKotlinAndroid :navigation:compileKotlinIosX64 --stacktrace # - name: Kover Xml Report From b5014b4c9931ea7ee17bb226f0939dd45df06554 Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sun, 21 Jan 2024 16:35:47 +0700 Subject: [PATCH 28/28] skip ios sample --- .github/workflows/sample.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/sample.yml b/.github/workflows/sample.yml index b3c99bd8..aff820ed 100644 --- a/.github/workflows/sample.yml +++ b/.github/workflows/sample.yml @@ -66,6 +66,7 @@ jobs: os: [ macos-12 ] runs-on: ${{ matrix.os }} timeout-minutes: 30 + if: ${{ false }} steps: - uses: actions/checkout@v3