diff --git a/decompose-router-wear/src/androidMain/kotlin/io/github/xxfast/decompose/router/wear/content/RoutedContent.kt b/decompose-router-wear/src/androidMain/kotlin/io/github/xxfast/decompose/router/wear/content/RoutedContent.kt index b1d7ba6..49cc885 100644 --- a/decompose-router-wear/src/androidMain/kotlin/io/github/xxfast/decompose/router/wear/content/RoutedContent.kt +++ b/decompose-router-wear/src/androidMain/kotlin/io/github/xxfast/decompose/router/wear/content/RoutedContent.kt @@ -17,12 +17,12 @@ import androidx.wear.compose.material.SwipeToDismissBox import androidx.wear.compose.material.SwipeToDismissKeys.Background import androidx.wear.compose.material.rememberSwipeToDismissBoxState import com.arkivanov.decompose.Child -import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.pop -import io.github.xxfast.decompose.LocalComponentContext import io.github.xxfast.decompose.router.LocalRouter +import io.github.xxfast.decompose.router.LocalRouterContext import io.github.xxfast.decompose.router.Router +import io.github.xxfast.decompose.router.RouterContext @OptIn(ExperimentalWearFoundationApi::class) @Composable @@ -31,9 +31,9 @@ fun RoutedContent( modifier: Modifier = Modifier, content: @Composable (C) -> Unit, ) { - val stack: ChildStack by router.stack - val active: Child.Created = stack.active - val background: Child.Created? = stack.backStack.lastOrNull() + val stack: ChildStack by router.stack + val active: Child.Created = stack.active + val background: Child.Created? = stack.backStack.lastOrNull() val holder: SaveableStateHolder = rememberSaveableStateHolder() holder.RetainStates(stack.getConfigurations()) @@ -55,7 +55,7 @@ fun RoutedContent( ) { isBackground -> val child = if (isBackground) requireNotNull(background) else active holder.SaveableStateProvider(child.configuration.key()) { - CompositionLocalProvider(LocalComponentContext provides child.instance) { + CompositionLocalProvider(LocalRouterContext provides child.instance) { HierarchicalFocusCoordinator(requiresFocus = { !isBackground }) { content(child.configuration) } diff --git a/decompose-router/api/android/decompose-router.api b/decompose-router/api/android/decompose-router.api index 13238f2..c9487ce 100644 --- a/decompose-router/api/android/decompose-router.api +++ b/decompose-router/api/android/decompose-router.api @@ -1,7 +1,3 @@ -public final class io/github/xxfast/decompose/NavigatorKt { - public static final fun getLocalComponentContext ()Landroidx/compose/runtime/ProvidableCompositionLocal; -} - public final class io/github/xxfast/decompose/router/Router : com/arkivanov/decompose/router/stack/StackNavigation { public static final field $stable I public fun (Lcom/arkivanov/decompose/router/stack/StackNavigation;Landroidx/compose/runtime/State;)V @@ -11,6 +7,25 @@ public final class io/github/xxfast/decompose/router/Router : com/arkivanov/deco public fun unsubscribe (Lkotlin/jvm/functions/Function1;)V } +public final class io/github/xxfast/decompose/router/RouterContext : com/arkivanov/decompose/ComponentContext { + public static final field $stable I + public fun (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/statekeeper/StateKeeper;Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Lcom/arkivanov/essenty/backhandler/BackHandler;)V + public synthetic fun (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/statekeeper/StateKeeper;Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Lcom/arkivanov/essenty/backhandler/BackHandler;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getBackHandler ()Lcom/arkivanov/essenty/backhandler/BackHandler; + public fun getInstanceKeeper ()Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper; + public fun getLifecycle ()Lcom/arkivanov/essenty/lifecycle/Lifecycle; + public fun getStateKeeper ()Lcom/arkivanov/essenty/statekeeper/StateKeeper; +} + +public final class io/github/xxfast/decompose/router/RouterContextExtKt { + public static final fun defaultRouterContext (Landroidx/activity/ComponentActivity;)Lio/github/xxfast/decompose/router/RouterContext; + public static final fun defaultRouterContext (Landroidx/fragment/app/Fragment;Landroidx/activity/OnBackPressedDispatcher;)Lio/github/xxfast/decompose/router/RouterContext; +} + +public final class io/github/xxfast/decompose/router/RouterContextKt { + public static final fun getLocalRouterContext ()Landroidx/compose/runtime/ProvidableCompositionLocal; +} + public final class io/github/xxfast/decompose/router/RouterKt { public static final fun getLocalRouter ()Landroidx/compose/runtime/ProvidableCompositionLocal; public static final fun rememberOnRoute (Lkotlin/reflect/KClass;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; diff --git a/decompose-router/api/desktop/decompose-router.api b/decompose-router/api/desktop/decompose-router.api index 4c08bfb..8773b88 100644 --- a/decompose-router/api/desktop/decompose-router.api +++ b/decompose-router/api/desktop/decompose-router.api @@ -1,7 +1,3 @@ -public final class io/github/xxfast/decompose/NavigatorKt { - public static final fun getLocalComponentContext ()Landroidx/compose/runtime/ProvidableCompositionLocal; -} - public final class io/github/xxfast/decompose/router/Router : com/arkivanov/decompose/router/stack/StackNavigation { public static final field $stable I public fun (Lcom/arkivanov/decompose/router/stack/StackNavigation;Landroidx/compose/runtime/State;)V @@ -11,6 +7,20 @@ public final class io/github/xxfast/decompose/router/Router : com/arkivanov/deco public fun unsubscribe (Lkotlin/jvm/functions/Function1;)V } +public final class io/github/xxfast/decompose/router/RouterContext : com/arkivanov/decompose/ComponentContext { + public static final field $stable I + public fun (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/statekeeper/StateKeeper;Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Lcom/arkivanov/essenty/backhandler/BackHandler;)V + public synthetic fun (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/statekeeper/StateKeeper;Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Lcom/arkivanov/essenty/backhandler/BackHandler;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getBackHandler ()Lcom/arkivanov/essenty/backhandler/BackHandler; + public fun getInstanceKeeper ()Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper; + public fun getLifecycle ()Lcom/arkivanov/essenty/lifecycle/Lifecycle; + public fun getStateKeeper ()Lcom/arkivanov/essenty/statekeeper/StateKeeper; +} + +public final class io/github/xxfast/decompose/router/RouterContextKt { + public static final fun getLocalRouterContext ()Landroidx/compose/runtime/ProvidableCompositionLocal; +} + public final class io/github/xxfast/decompose/router/RouterKt { public static final fun getLocalRouter ()Landroidx/compose/runtime/ProvidableCompositionLocal; public static final fun rememberOnRoute (Lkotlin/reflect/KClass;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; diff --git a/decompose-router/build.gradle.kts b/decompose-router/build.gradle.kts index 1979232..0a32093 100644 --- a/decompose-router/build.gradle.kts +++ b/decompose-router/build.gradle.kts @@ -66,6 +66,7 @@ kotlin { implementation(libs.decompose.compose.multiplatform) implementation(libs.androidx.activity.ktx) implementation(libs.androidx.activity.compose) + implementation(libs.androidx.fragment.ktx) } } diff --git a/decompose-router/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/TestActivity.kt b/decompose-router/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/TestActivity.kt index ae08da3..2550e35 100644 --- a/decompose-router/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/TestActivity.kt +++ b/decompose-router/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/TestActivity.kt @@ -7,19 +7,20 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.CompositionLocalProvider import androidx.core.view.WindowCompat -import com.arkivanov.decompose.DefaultComponentContext -import com.arkivanov.decompose.defaultComponentContext +import io.github.xxfast.decompose.router.LocalRouterContext +import io.github.xxfast.decompose.router.RouterContext +import io.github.xxfast.decompose.router.defaultRouterContext import io.github.xxfast.decompose.screen.HomeScreen class TestActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) - val rootComponentContext: DefaultComponentContext = defaultComponentContext() + val rootRouterContext: RouterContext = defaultRouterContext() setContent { Surface { - CompositionLocalProvider(LocalComponentContext provides rootComponentContext) { + CompositionLocalProvider(LocalRouterContext provides rootRouterContext) { MaterialTheme { HomeScreen() } diff --git a/decompose-router/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/screen/HomeScreen.kt b/decompose-router/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/screen/HomeScreen.kt index dab153e..e9f3619 100644 --- a/decompose-router/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/screen/HomeScreen.kt +++ b/decompose-router/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/screen/HomeScreen.kt @@ -7,7 +7,7 @@ import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.slid import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.stackAnimation import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.router.stack.push -import io.github.xxfast.decompose.LocalComponentContext +import io.github.xxfast.decompose.router.LocalRouterContext import io.github.xxfast.decompose.router.Router import io.github.xxfast.decompose.router.content.RoutedContent import io.github.xxfast.decompose.router.rememberRouter @@ -26,7 +26,7 @@ fun HomeScreen() { animation = predictiveBackAnimation( animation = stackAnimation(slide()), onBack = { router.pop() }, - backHandler = LocalComponentContext.current.backHandler + backHandler = LocalRouterContext.current.backHandler ) ) { screen -> when (screen) { diff --git a/decompose-router/src/androidMain/kotlin/io/github/xxfast/decompose/router/RouterContextExt.kt b/decompose-router/src/androidMain/kotlin/io/github/xxfast/decompose/router/RouterContextExt.kt new file mode 100644 index 0000000..4f8b24b --- /dev/null +++ b/decompose-router/src/androidMain/kotlin/io/github/xxfast/decompose/router/RouterContextExt.kt @@ -0,0 +1,14 @@ +package io.github.xxfast.decompose.router + +import androidx.activity.ComponentActivity +import androidx.activity.OnBackPressedDispatcher +import androidx.fragment.app.Fragment +import com.arkivanov.decompose.defaultComponentContext + +fun ComponentActivity.defaultRouterContext(): RouterContext = + RouterContext(defaultComponentContext()) + +fun Fragment.defaultRouterContext( + onBackPressedDispatcher: OnBackPressedDispatcher?, +): RouterContext = + RouterContext(defaultComponentContext(onBackPressedDispatcher)) diff --git a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/Navigator.kt b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/Navigator.kt deleted file mode 100644 index 61b2a24..0000000 --- a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/Navigator.kt +++ /dev/null @@ -1,45 +0,0 @@ -package io.github.xxfast.decompose - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ProvidableCompositionLocal -import androidx.compose.runtime.State -import androidx.compose.runtime.remember -import androidx.compose.runtime.staticCompositionLocalOf -import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState -import com.arkivanov.decompose.router.stack.ChildStack -import com.arkivanov.decompose.router.stack.StackNavigationSource -import com.arkivanov.decompose.router.stack.childStack -import com.arkivanov.essenty.parcelable.Parcelable -import kotlin.reflect.KClass - -/*** - * Compositional local for component context - * - * Based on [Arkadii](https://github.com/arkivanov)'s [article](https://proandroiddev.com/a-comprehensive-hundred-line-navigation-for-jetpack-desktop-compose-5b723c4f256e) - * Original [source](https://github.com/arkivanov/ComposeNavigatorExample/blob/d786d92632fe22e4d7874645ba2071fb813f9ace/navigator/src/commonMain/kotlin/com/arkivanov/composenavigatorexample/navigator/Navigator.kt) - */ -val LocalComponentContext: ProvidableCompositionLocal = - staticCompositionLocalOf { error("Root component context was not provided") } - -@Composable -internal fun rememberChildStack( - type: KClass, - source: StackNavigationSource, - initialStack: () -> List, - key: String = "DefaultChildStack", - handleBackButton: Boolean = false, -): State> { - val componentContext = LocalComponentContext.current - - return remember { - componentContext.childStack( - source = source, - initialStack = initialStack, - configurationClass = type, - key = key, - handleBackButton = handleBackButton, - childFactory = { _, childComponentContext -> childComponentContext }, - ) - }.subscribeAsState() -} diff --git a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/Router.kt b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/Router.kt index aa924df..d6872be 100644 --- a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/Router.kt +++ b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/Router.kt @@ -5,18 +5,20 @@ import androidx.compose.runtime.DisallowComposableCalls import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.staticCompositionLocalOf -import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.value.Value +import com.arkivanov.decompose.value.observe import com.arkivanov.essenty.instancekeeper.InstanceKeeper import com.arkivanov.essenty.instancekeeper.InstanceKeeper.Instance import com.arkivanov.essenty.instancekeeper.getOrCreate +import com.arkivanov.essenty.lifecycle.Lifecycle import com.arkivanov.essenty.parcelable.Parcelable import com.arkivanov.essenty.statekeeper.StateKeeper -import io.github.xxfast.decompose.LocalComponentContext -import io.github.xxfast.decompose.rememberChildStack import kotlin.reflect.KClass /*** @@ -28,7 +30,7 @@ import kotlin.reflect.KClass */ class Router( private val navigator: StackNavigation, - val stack: State>, + val stack: State>, ) : StackNavigation by navigator /*** @@ -51,16 +53,31 @@ fun rememberRouter( stack: List, handleBackButton: Boolean = true ): Router { - val navigator: StackNavigation = remember { StackNavigation() } - val childStackState: State> = rememberChildStack( - source = navigator, - initialStack = { stack }, - key = key.toString(), // Has to use strings for Android 😢 - handleBackButton = handleBackButton, - type = type, - ) + val routerContext = LocalRouterContext.current + val keyStr = key.toString() - return remember { Router(navigator = navigator, stack = childStackState) } + return remember { + routerContext.getOrCreate(key = keyStr) { + val navigation = StackNavigation() + Router( + navigator = navigation, + stack = routerContext.childStack( + source = navigation, + initialStack = { stack }, + configurationClass = type, + key = keyStr, + handleBackButton = handleBackButton, + childFactory = { _, childComponentContext -> RouterContext(childComponentContext) }, + ).asState(routerContext.lifecycle), + ) + } + } +} + +private fun Value.asState(lifecycle: Lifecycle): State { + val state = mutableStateOf(value) + observe(lifecycle = lifecycle) { state.value = it } + return state } /*** @@ -77,7 +94,7 @@ fun rememberOnRoute( key: Any = type.key, block: @DisallowComposableCalls (savedState: SavedStateHandle) -> T ): T { - val component: ComponentContext = LocalComponentContext.current + val component: RouterContext = LocalRouterContext.current val stateKeeper: StateKeeper = component.stateKeeper val instanceKeeper: InstanceKeeper = component.instanceKeeper val instanceKey = "$key.instance" diff --git a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/RouterContext.kt b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/RouterContext.kt new file mode 100644 index 0000000..fa7f065 --- /dev/null +++ b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/RouterContext.kt @@ -0,0 +1,43 @@ +package io.github.xxfast.decompose.router + +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.staticCompositionLocalOf +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.DefaultComponentContext +import com.arkivanov.essenty.backhandler.BackHandler +import com.arkivanov.essenty.instancekeeper.InstanceKeeper +import com.arkivanov.essenty.lifecycle.Lifecycle +import com.arkivanov.essenty.statekeeper.StateKeeper + +class RouterContext internal constructor( + private val delegate: ComponentContext, +) : ComponentContext by delegate { + + constructor( + lifecycle: Lifecycle, + stateKeeper: StateKeeper? = null, + instanceKeeper: InstanceKeeper? = null, + backHandler: BackHandler? = null, + ) : this(DefaultComponentContext(lifecycle, stateKeeper, instanceKeeper, backHandler)) + + internal val storage: MutableMap = HashMap() +} + +internal inline fun RouterContext.getOrCreate(key: Any, factory: () -> T) : T { + var instance: T? = storage[key] as T? + if (instance == null) { + instance = factory() + storage[key] = instance + } + + return instance +} + +/*** + * Compositional local for [RouterContext]. + * + * Based on [Arkadii](https://github.com/arkivanov)'s [article](https://proandroiddev.com/a-comprehensive-hundred-line-navigation-for-jetpack-desktop-compose-5b723c4f256e) + * Original [source](https://github.com/arkivanov/ComposeNavigatorExample/blob/d786d92632fe22e4d7874645ba2071fb813f9ace/navigator/src/commonMain/kotlin/com/arkivanov/composenavigatorexample/navigator/Navigator.kt) + */ +val LocalRouterContext: ProvidableCompositionLocal = + staticCompositionLocalOf { error("Root RouterContext was not provided") } diff --git a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/content/RoutedContent.kt b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/content/RoutedContent.kt index 93ec7af..5f10ed7 100644 --- a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/content/RoutedContent.kt +++ b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/content/RoutedContent.kt @@ -3,13 +3,13 @@ package io.github.xxfast.decompose.router.content import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier -import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.jetbrains.stack.Children import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.StackAnimation import com.arkivanov.essenty.parcelable.Parcelable -import io.github.xxfast.decompose.LocalComponentContext import io.github.xxfast.decompose.router.LocalRouter +import io.github.xxfast.decompose.router.LocalRouterContext import io.github.xxfast.decompose.router.Router +import io.github.xxfast.decompose.router.RouterContext /*** * Composable to hoist content that are navigated by the router @@ -23,7 +23,7 @@ import io.github.xxfast.decompose.router.Router fun RoutedContent( router: Router, modifier: Modifier = Modifier, - animation: StackAnimation? = null, + animation: StackAnimation? = null, content: @Composable (C) -> Unit, ) { CompositionLocalProvider(LocalRouter provides router) { @@ -32,7 +32,7 @@ fun RoutedContent( modifier = modifier, animation = animation, ) { child -> - CompositionLocalProvider(LocalComponentContext provides child.instance) { + CompositionLocalProvider(LocalRouterContext provides child.instance) { content(child.configuration) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 06cc0ef..55ff12b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,11 @@ [versions] agp = "8.1.1" androidx-activity ="1.7.2" +androidx-fragment ="1.6.1" compose-multiplatform = "1.5.0-beta02" compose-test-rule = "1.5.0" -decompose = "2.1.0-compose-experimental-alpha-07" -essenty = "1.2.0-alpha-06" +decompose = "2.1.0-compose-experimental" +essenty = "1.2.0" horologist = "0.4.12" kotlin = "1.9.0" wear-compose = "1.3.0-alpha03" @@ -13,6 +14,7 @@ wear-compose = "1.3.0-alpha03" agp = { module = "com.android.tools.build:gradle", version.ref = "agp" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version = "androidx-activity" } +androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version = "androidx-fragment" } compose-multiplatform = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose-multiplatform" } compose-ui-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose-test-rule" } compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose-test-rule" }