diff --git a/compose/build.gradle.kts b/compose/build.gradle.kts index 3eeb88b8..c2515c96 100644 --- a/compose/build.gradle.kts +++ b/compose/build.gradle.kts @@ -8,6 +8,11 @@ plugins { android { configureAndroidLibrary(this) + namespace = "${Config.namespace}.compose" + + buildFeatures { + compose = true + } } kotlin { @@ -43,14 +48,6 @@ kotlin { } } -android { - namespace = "${Config.namespace}.compose" - - buildFeatures { - compose = true - } -} - dependencies { debugImplementation(libs.compose.tooling) } diff --git a/compose/src/androidMain/kotlin/pro/respawn/flowmvi/compose/android/AndroidInterop.kt b/compose/src/androidMain/kotlin/pro/respawn/flowmvi/compose/android/AndroidInterop.kt index 8a445a3a..9a3e3c64 100644 --- a/compose/src/androidMain/kotlin/pro/respawn/flowmvi/compose/android/AndroidInterop.kt +++ b/compose/src/androidMain/kotlin/pro/respawn/flowmvi/compose/android/AndroidInterop.kt @@ -3,10 +3,10 @@ package pro.respawn.flowmvi.compose.android import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.repeatOnLifecycle -import pro.respawn.flowmvi.compose.api.SubscriberLifecycleOwner +import pro.respawn.flowmvi.compose.api.SubscriberLifecycle import pro.respawn.flowmvi.compose.api.SubscriptionMode -public fun LifecycleOwner.asSubscriberOwner(): SubscriberLifecycleOwner = SubscriberLifecycleOwner { mode, block -> +public fun LifecycleOwner.asSubscriberOwner(): SubscriberLifecycle = SubscriberLifecycle { mode, block -> repeatOnLifecycle(mode.asLifecycleState, block) } diff --git a/compose/src/androidMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.android.kt b/compose/src/androidMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.android.kt index f71ca858..0ef66d3c 100644 --- a/compose/src/androidMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.android.kt +++ b/compose/src/androidMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.android.kt @@ -3,7 +3,7 @@ package pro.respawn.flowmvi.compose.dsl import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalLifecycleOwner import pro.respawn.flowmvi.compose.android.asSubscriberOwner -import pro.respawn.flowmvi.compose.api.SubscriberLifecycleOwner +import pro.respawn.flowmvi.compose.api.SubscriberLifecycle -internal actual val PlatformLifecycle: SubscriberLifecycleOwner? +internal actual val PlatformLifecycle: SubscriberLifecycle? @Composable get() = LocalLifecycleOwner.current.asSubscriberOwner() diff --git a/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/api/SubscriberLifecycleOwner.kt b/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/api/SubscriberLifecycle.kt similarity index 80% rename from compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/api/SubscriberLifecycleOwner.kt rename to compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/api/SubscriberLifecycle.kt index 7a432d0c..10a47bb6 100644 --- a/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/api/SubscriberLifecycleOwner.kt +++ b/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/api/SubscriberLifecycle.kt @@ -2,7 +2,7 @@ package pro.respawn.flowmvi.compose.api import kotlinx.coroutines.CoroutineScope -public fun interface SubscriberLifecycleOwner { +public fun interface SubscriberLifecycle { public suspend fun repeatOnLifecycle(mode: SubscriptionMode, block: suspend CoroutineScope.() -> Unit) } diff --git a/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/CompositionLocals.kt b/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/CompositionLocals.kt new file mode 100644 index 00000000..2c8c45f9 --- /dev/null +++ b/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/CompositionLocals.kt @@ -0,0 +1,14 @@ +package pro.respawn.flowmvi.compose.dsl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import pro.respawn.flowmvi.compose.api.SubscriberLifecycle + +@Composable +public fun ProvideSubscriberLifecycle( + lifecycleOwner: SubscriberLifecycle, + content: @Composable () -> Unit +): Unit = CompositionLocalProvider( + LocalSubscriberLifecycle provides lifecycleOwner, + content = content, +) diff --git a/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/ImmediateLifecycle.kt b/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/ImmediateLifecycle.kt new file mode 100644 index 00000000..cf0f6dcf --- /dev/null +++ b/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/ImmediateLifecycle.kt @@ -0,0 +1,7 @@ +package pro.respawn.flowmvi.compose.dsl + +import kotlinx.coroutines.coroutineScope +import pro.respawn.flowmvi.compose.api.SubscriberLifecycle + +@PublishedApi +internal val ImmediateLifecycle: SubscriberLifecycle = SubscriberLifecycle { _, block -> coroutineScope(block) } diff --git a/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/ImmediateLifecycleOwner.kt b/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/ImmediateLifecycleOwner.kt deleted file mode 100644 index fc138206..00000000 --- a/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/ImmediateLifecycleOwner.kt +++ /dev/null @@ -1,9 +0,0 @@ -package pro.respawn.flowmvi.compose.dsl - -import kotlinx.coroutines.coroutineScope -import pro.respawn.flowmvi.compose.api.SubscriberLifecycleOwner - -@PublishedApi -internal val ImmediateLifecycleOwner: SubscriberLifecycleOwner = SubscriberLifecycleOwner { _, block -> - coroutineScope(block) -} diff --git a/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.kt b/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.kt index d3d00693..e6223e8f 100644 --- a/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.kt +++ b/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.kt @@ -3,15 +3,15 @@ package pro.respawn.flowmvi.compose.dsl import androidx.compose.runtime.Composable import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.staticCompositionLocalOf -import pro.respawn.flowmvi.compose.api.SubscriberLifecycleOwner +import pro.respawn.flowmvi.compose.api.SubscriberLifecycle -public val LocalSubscriberLifecycle: ProvidableCompositionLocal = staticCompositionLocalOf { +public val LocalSubscriberLifecycle: ProvidableCompositionLocal = staticCompositionLocalOf { null } -@PublishedApi -internal val CurrentLifecycle: SubscriberLifecycleOwner - @Composable get() = LocalSubscriberLifecycle.current ?: PlatformLifecycle ?: ImmediateLifecycleOwner - @get:Composable -internal expect val PlatformLifecycle: SubscriberLifecycleOwner? +internal expect val PlatformLifecycle: SubscriberLifecycle? + +@PublishedApi +internal val CurrentLifecycle: SubscriberLifecycle + @Composable get() = LocalSubscriberLifecycle.current ?: PlatformLifecycle ?: ImmediateLifecycle diff --git a/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/SubscribeDsl.kt b/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/SubscribeDsl.kt index 5cc5e001..b577b385 100644 --- a/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/SubscribeDsl.kt +++ b/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/SubscribeDsl.kt @@ -16,7 +16,7 @@ import pro.respawn.flowmvi.api.ImmutableStore import pro.respawn.flowmvi.api.MVIAction import pro.respawn.flowmvi.api.MVIIntent import pro.respawn.flowmvi.api.MVIState -import pro.respawn.flowmvi.compose.api.SubscriberLifecycleOwner +import pro.respawn.flowmvi.compose.api.SubscriberLifecycle import pro.respawn.flowmvi.compose.api.SubscriptionMode import pro.respawn.flowmvi.dsl.subscribe import pro.respawn.flowmvi.util.immediateOrDefault @@ -27,14 +27,14 @@ import pro.respawn.flowmvi.util.immediateOrDefault @FlowMVIDSL public inline fun ImmutableStore.subscribe( mode: SubscriptionMode = SubscriptionMode.Started, - owner: SubscriberLifecycleOwner = CurrentLifecycle, + lifecycle: SubscriberLifecycle = CurrentLifecycle, noinline consume: suspend CoroutineScope.(action: A) -> Unit, ): State { val state = remember(this) { mutableStateOf(state) } val block by rememberUpdatedState(consume) - LaunchedEffect(this@subscribe, mode, owner) { + LaunchedEffect(this@subscribe, mode, lifecycle) { withContext(Dispatchers.Main.immediateOrDefault) { - owner.repeatOnLifecycle(mode) { + lifecycle.repeatOnLifecycle(mode) { subscribe( store = this@subscribe, consume = { block(it) }, @@ -51,13 +51,13 @@ public inline fun ImmutableStore ImmutableStore.subscribe( - mode: SubscriptionMode = SubscriptionMode.Visible, - owner: SubscriberLifecycleOwner = CurrentLifecycle, + mode: SubscriptionMode = SubscriptionMode.Started, + lifecycle: SubscriberLifecycle = CurrentLifecycle, ): State { val state = remember(this) { mutableStateOf(state) } - LaunchedEffect(this@subscribe, mode, owner) { + LaunchedEffect(this@subscribe, mode, lifecycle) { withContext(Dispatchers.Main.immediateOrDefault) { - owner.repeatOnLifecycle(mode) { + lifecycle.repeatOnLifecycle(mode) { subscribe( store = this@subscribe, render = { state.value = it } diff --git a/compose/src/jsMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.js.kt b/compose/src/jsMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.js.kt index fd867f21..073c4586 100644 --- a/compose/src/jsMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.js.kt +++ b/compose/src/jsMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.js.kt @@ -1,6 +1,6 @@ package pro.respawn.flowmvi.compose.dsl import androidx.compose.runtime.Composable -import pro.respawn.flowmvi.compose.api.SubscriberLifecycleOwner +import pro.respawn.flowmvi.compose.api.SubscriberLifecycle -internal actual val PlatformLifecycle: SubscriberLifecycleOwner? @Composable get() = null +internal actual val PlatformLifecycle: SubscriberLifecycle? @Composable get() = null diff --git a/compose/src/jvmMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.jvm.kt b/compose/src/jvmMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.jvm.kt index fd867f21..073c4586 100644 --- a/compose/src/jvmMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.jvm.kt +++ b/compose/src/jvmMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.jvm.kt @@ -1,6 +1,6 @@ package pro.respawn.flowmvi.compose.dsl import androidx.compose.runtime.Composable -import pro.respawn.flowmvi.compose.api.SubscriberLifecycleOwner +import pro.respawn.flowmvi.compose.api.SubscriberLifecycle -internal actual val PlatformLifecycle: SubscriberLifecycleOwner? @Composable get() = null +internal actual val PlatformLifecycle: SubscriberLifecycle? @Composable get() = null diff --git a/compose/src/nativeMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.native.kt b/compose/src/nativeMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.native.kt index fd867f21..073c4586 100644 --- a/compose/src/nativeMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.native.kt +++ b/compose/src/nativeMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.native.kt @@ -1,6 +1,6 @@ package pro.respawn.flowmvi.compose.dsl import androidx.compose.runtime.Composable -import pro.respawn.flowmvi.compose.api.SubscriberLifecycleOwner +import pro.respawn.flowmvi.compose.api.SubscriberLifecycle -internal actual val PlatformLifecycle: SubscriberLifecycleOwner? @Composable get() = null +internal actual val PlatformLifecycle: SubscriberLifecycle? @Composable get() = null diff --git a/compose/src/wasmJsMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.wasmJs.kt b/compose/src/wasmJsMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.wasmJs.kt index fd867f21..073c4586 100644 --- a/compose/src/wasmJsMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.wasmJs.kt +++ b/compose/src/wasmJsMain/kotlin/pro/respawn/flowmvi/compose/dsl/LocalSubscriberLifecycle.wasmJs.kt @@ -1,6 +1,6 @@ package pro.respawn.flowmvi.compose.dsl import androidx.compose.runtime.Composable -import pro.respawn.flowmvi.compose.api.SubscriberLifecycleOwner +import pro.respawn.flowmvi.compose.api.SubscriberLifecycle -internal actual val PlatformLifecycle: SubscriberLifecycleOwner? @Composable get() = null +internal actual val PlatformLifecycle: SubscriberLifecycle? @Composable get() = null diff --git a/essenty/essenty-compose/build.gradle.kts b/essenty/essenty-compose/build.gradle.kts new file mode 100644 index 00000000..f8f6f49a --- /dev/null +++ b/essenty/essenty-compose/build.gradle.kts @@ -0,0 +1,54 @@ +plugins { + id(libs.plugins.kotlinMultiplatform.id) + id(libs.plugins.androidLibrary.id) + alias(libs.plugins.jetbrainsCompose) + id("maven-publish") + signing +} + +android { + configureAndroidLibrary(this) + namespace = "${Config.namespace}.decompose.compose" + + buildFeatures { + compose = true + } +} + +kotlin { + configureMultiplatform( + ext = this, + tvOs = false, + watchOs = false, + linux = false, + windows = false, + ) + sourceSets { + androidMain.dependencies { + implementation(libs.compose.foundation) + implementation(libs.compose.preview) + implementation(libs.compose.lifecycle.runtime) + api(projects.android) + } + commonMain.dependencies { + implementation(compose.runtime) + implementation(compose.foundation) + api(projects.core) + api(projects.compose) + + api(libs.essenty.lifecycle) + api(libs.essenty.lifecycle.coroutines) + api(libs.essenty.instancekeeper) + } + jvmMain.dependencies { + implementation(compose.desktop.common) + implementation(libs.compose.lifecycle.runtime) + } + } +} + +publishMultiplatform() + +dependencies { + +} diff --git a/essenty/essenty-compose/src/commonMain/kotlin/pro/respawn/flowmvi/decompose/compose/CompositionLocals.kt b/essenty/essenty-compose/src/commonMain/kotlin/pro/respawn/flowmvi/decompose/compose/CompositionLocals.kt new file mode 100644 index 00000000..d93bae50 --- /dev/null +++ b/essenty/essenty-compose/src/commonMain/kotlin/pro/respawn/flowmvi/decompose/compose/CompositionLocals.kt @@ -0,0 +1,15 @@ +package pro.respawn.flowmvi.decompose.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import com.arkivanov.essenty.lifecycle.LifecycleOwner +import pro.respawn.flowmvi.compose.dsl.LocalSubscriberLifecycle + +@Composable +public fun ProvideSubscriberLifecycle( + owner: LifecycleOwner, + content: @Composable () -> Unit +): Unit = CompositionLocalProvider( + LocalSubscriberLifecycle provides owner.asSubscriberLifecycle, + content = content, +) diff --git a/essenty/essenty-compose/src/commonMain/kotlin/pro/respawn/flowmvi/decompose/compose/Lifecycle.kt b/essenty/essenty-compose/src/commonMain/kotlin/pro/respawn/flowmvi/decompose/compose/Lifecycle.kt new file mode 100644 index 00000000..dd0d3348 --- /dev/null +++ b/essenty/essenty-compose/src/commonMain/kotlin/pro/respawn/flowmvi/decompose/compose/Lifecycle.kt @@ -0,0 +1,25 @@ +package pro.respawn.flowmvi.decompose.compose + +import com.arkivanov.essenty.lifecycle.Lifecycle +import com.arkivanov.essenty.lifecycle.LifecycleOwner +import com.arkivanov.essenty.lifecycle.coroutines.repeatOnLifecycle +import pro.respawn.flowmvi.compose.api.SubscriberLifecycle +import pro.respawn.flowmvi.compose.api.SubscriptionMode + +public val LifecycleOwner.asSubscriberLifecycle: SubscriberLifecycle + get() = SubscriberLifecycle { mode, block -> repeatOnLifecycle(mode.asEssentyLifecycle, block = block) } + +public val SubscriptionMode.asEssentyLifecycle: Lifecycle.State + get() = when (this) { + SubscriptionMode.Immediate -> Lifecycle.State.CREATED + SubscriptionMode.Started -> Lifecycle.State.STARTED + SubscriptionMode.Visible -> Lifecycle.State.RESUMED + } +public val Lifecycle.State.asSubscriptionMode: SubscriptionMode + get() = when (this) { + Lifecycle.State.CREATED -> SubscriptionMode.Immediate + Lifecycle.State.STARTED -> SubscriptionMode.Started + Lifecycle.State.RESUMED -> SubscriptionMode.Visible + Lifecycle.State.DESTROYED, + Lifecycle.State.INITIALIZED -> error("Essenty does not provide support for using $name as subscriber lifecycle") + } diff --git a/essenty/essenty-compose/src/commonMain/kotlin/pro/respawn/flowmvi/decompose/compose/SubscribeDsl.kt b/essenty/essenty-compose/src/commonMain/kotlin/pro/respawn/flowmvi/decompose/compose/SubscribeDsl.kt new file mode 100644 index 00000000..84f13736 --- /dev/null +++ b/essenty/essenty-compose/src/commonMain/kotlin/pro/respawn/flowmvi/decompose/compose/SubscribeDsl.kt @@ -0,0 +1,58 @@ +package pro.respawn.flowmvi.decompose.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import com.arkivanov.essenty.lifecycle.Lifecycle +import com.arkivanov.essenty.lifecycle.LifecycleOwner +import kotlinx.coroutines.CoroutineScope +import pro.respawn.flowmvi.api.FlowMVIDSL +import pro.respawn.flowmvi.api.ImmutableStore +import pro.respawn.flowmvi.api.MVIAction +import pro.respawn.flowmvi.api.MVIIntent +import pro.respawn.flowmvi.api.MVIState +import pro.respawn.flowmvi.compose.dsl.subscribe +import pro.respawn.flowmvi.dsl.subscribe + +/** + * A function to subscribe to the store that follows the system lifecycle. + * + * * This function will assign the store a new subscriber when invoked, then populate the returned [State] with new states. + * * Provided [consume] parameter will be used to consume actions that come from the store. + * * Store's subscribers will **not** wait until the store is launched when they subscribe to the store. + * Such subscribers will not receive state updates or actions. Don't forget to launch the store. + * + * @param lifecycleState the minimum lifecycle state that should be reached in order to subscribe to the store, + * upon leaving that state, the function will unsubscribe. + * @param consume a lambda to consume actions with. + * @return the [State] that contains the current state. + * @see ImmutableStore.subscribe + * @see subscribe + */ +@Suppress("ComposableParametersOrdering") +@Composable +@FlowMVIDSL +public inline fun ImmutableStore.subscribe( + lifecycleOwner: LifecycleOwner, + lifecycleState: Lifecycle.State = Lifecycle.State.STARTED, + noinline consume: suspend CoroutineScope.(action: A) -> Unit, +): State = subscribe(lifecycleState.asSubscriptionMode, lifecycleOwner.asSubscriberLifecycle, consume) + +/** + * A function to subscribe to the store that follows the system lifecycle. + * + * * This function will not collect [MVIAction]s. + * * This function will assign the store a new subscriber when invoked, then populate the returned [State] with new states. + * * Store's subscribers will **not** wait until the store is launched when they subscribe to the store. + * Such subscribers will not receive state updates or actions. Don't forget to launch the store. + * @param lifecycleState the minimum lifecycle state that should be reached in order to subscribe to the store, + * upon leaving that state, the function will unsubscribe. + * @return the [State] that contains the current state. + * @see ImmutableStore.subscribe + * @see subscribe + */ +@Composable +@FlowMVIDSL +public inline fun ImmutableStore.subscribe( + lifecycleOwner: LifecycleOwner, + lifecycleState: Lifecycle.State = Lifecycle.State.CREATED, +): State = subscribe(lifecycleState.asSubscriptionMode, lifecycleOwner.asSubscriberLifecycle) diff --git a/settings.gradle.kts b/settings.gradle.kts index 8405949a..4a45158b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -43,7 +43,8 @@ include(":android-compose") include(":android-view") include(":compose") include(":savedstate") -include(":decompose") +include(":essenty") +include(":essenty:essenty-compose") include(":debugger:app") include(":debugger:debugger-client") include(":debugger:debugger-plugin")