From 8e04963382696896ead342780611434d15652f3b Mon Sep 17 00:00:00 2001 From: xxfast Date: Thu, 30 Nov 2023 11:35:02 +1100 Subject: [PATCH] Migrate from parcelable and parcelize to kotlinx serializable --- app/build.gradle.kts | 3 +- .../router/app/screens/HomeStateModels.kt | 13 ++++----- .../app/screens/details/DetailStateModels.kt | 7 ++--- .../router/app/screens/list/ListScreen.kt | 9 ++++-- .../app/screens/list/ListStateModels.kt | 6 ++-- .../app/screens/nested/NestedScreenModels.kt | 7 ++--- build.gradle.kts | 1 + decompose-router-wear/build.gradle.kts | 1 - decompose-router/build.gradle.kts | 2 +- .../io/github/xxfast/decompose/router/Key.kt | 2 +- .../io/github/xxfast/decompose/router/Key.kt | 2 +- .../github/xxfast/decompose/router/Router.kt | 29 ++++++++++++------- .../xxfast/decompose/router/RouterContext.kt | 6 ++-- .../xxfast/decompose/router/SavedState.kt | 22 ++++---------- .../decompose/router/content/RoutedContent.kt | 3 +- .../io/github/xxfast/decompose/router/Key.kt | 2 +- .../io/github/xxfast/decompose/router/Key.kt | 2 +- .../io/github/xxfast/decompose/router/Key.kt | 2 +- gradle/libs.versions.toml | 1 - 19 files changed, 59 insertions(+), 61 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cb104c5..bbda33b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -4,9 +4,9 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat.Msi plugins { kotlin("multiplatform") + kotlin("plugin.serialization") id("com.android.application") id("org.jetbrains.compose") - id("kotlin-parcelize") } @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) @@ -70,7 +70,6 @@ kotlin { implementation(libs.decompose) implementation(libs.decompose.compose.multiplatform) - implementation(libs.essenty.parcelable) } } diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/HomeStateModels.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/HomeStateModels.kt index e5045aa..3d10fc3 100644 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/HomeStateModels.kt +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/HomeStateModels.kt @@ -1,12 +1,11 @@ package io.github.xxfast.decompose.router.app.screens -import com.arkivanov.essenty.parcelable.Parcelable -import com.arkivanov.essenty.parcelable.Parcelize +import kotlinx.serialization.Serializable -@Parcelize -sealed class HomeScreens: Parcelable { - data object List: HomeScreens() - data object Nested: HomeScreens() - data class Details(val number: Int): HomeScreens() +@Serializable +sealed class HomeScreens { + @Serializable data object List: HomeScreens() + @Serializable data object Nested: HomeScreens() + @Serializable data class Details(val number: Int): HomeScreens() } diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/details/DetailStateModels.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/details/DetailStateModels.kt index b5aa0bb..2bcf6c6 100644 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/details/DetailStateModels.kt +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/details/DetailStateModels.kt @@ -1,7 +1,6 @@ package io.github.xxfast.decompose.router.app.screens.details -import com.arkivanov.essenty.parcelable.Parcelable -import com.arkivanov.essenty.parcelable.Parcelize +import kotlinx.serialization.Serializable -@Parcelize -data class DetailState(val count: Int): Parcelable +@Serializable +data class DetailState(val count: Int) diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/list/ListScreen.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/list/ListScreen.kt index c40f031..9d9ae47 100644 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/list/ListScreen.kt +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/list/ListScreen.kt @@ -35,14 +35,19 @@ import io.github.xxfast.decompose.router.app.screens.FAVORITE_TAG import io.github.xxfast.decompose.router.app.screens.LIST_TAG import io.github.xxfast.decompose.router.app.screens.TITLE_BAR_TAG import io.github.xxfast.decompose.router.app.screens.TOOLBAR_TAG +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.serializer -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, InternalSerializationApi::class) @Composable fun ListScreen( onSelect: (count: Int) -> Unit, onSelectColor: () -> Unit, ) { - val instance: ListInstance = rememberOnRoute(ListInstance::class) { savedState -> + val instance: ListInstance = rememberOnRoute( + type = ListInstance::class, + strategy = ListState::class.serializer() + ) { savedState -> ListInstance(savedState) } diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/list/ListStateModels.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/list/ListStateModels.kt index 78efc4e..d77deed 100644 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/list/ListStateModels.kt +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/list/ListStateModels.kt @@ -1,9 +1,7 @@ package io.github.xxfast.decompose.router.app.screens.list -import com.arkivanov.essenty.parcelable.Parcelable -import com.arkivanov.essenty.parcelable.Parcelize +import kotlinx.serialization.Serializable -@Parcelize -data class ListState(val items: List? = Loading): Parcelable +@Serializable data class ListState(val items: List? = Loading) val Loading: Nothing? = null diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/nested/NestedScreenModels.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/nested/NestedScreenModels.kt index d6c1884..e01a409 100644 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/nested/NestedScreenModels.kt +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/nested/NestedScreenModels.kt @@ -1,10 +1,9 @@ package io.github.xxfast.decompose.router.app.screens.nested -import com.arkivanov.essenty.parcelable.Parcelable -import com.arkivanov.essenty.parcelable.Parcelize +import kotlinx.serialization.Serializable -@Parcelize -sealed class NestedScreens: Parcelable { +@Serializable +sealed class NestedScreens { data object Home: NestedScreens() data object Primary: NestedScreens() data object Secondary: NestedScreens() diff --git a/build.gradle.kts b/build.gradle.kts index f8a92d7..7defbca 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,6 +16,7 @@ buildscript { classpath(libs.agp) classpath(libs.compose.multiplatform) classpath(libs.kotlin.gradle.plugin) + classpath(libs.kotlinx.serialization) } } diff --git a/decompose-router-wear/build.gradle.kts b/decompose-router-wear/build.gradle.kts index fd13e64..efb510b 100644 --- a/decompose-router-wear/build.gradle.kts +++ b/decompose-router-wear/build.gradle.kts @@ -23,7 +23,6 @@ kotlin { implementation(libs.wear.compose.material) implementation(libs.wear.compose.ui.tooling) implementation(libs.androidx.activity.compose) - implementation(libs.essenty.parcelable) implementation(libs.decompose) implementation(libs.decompose.compose.multiplatform) } diff --git a/decompose-router/build.gradle.kts b/decompose-router/build.gradle.kts index 0a32093..93bbb5f 100644 --- a/decompose-router/build.gradle.kts +++ b/decompose-router/build.gradle.kts @@ -33,9 +33,9 @@ kotlin { dependencies { implementation(compose.runtime) implementation(compose.foundation) - implementation(libs.essenty.parcelable) implementation(libs.decompose) implementation(libs.decompose.compose.multiplatform) + implementation(libs.kotlinx.serialization) } } diff --git a/decompose-router/src/androidMain/kotlin/io/github/xxfast/decompose/router/Key.kt b/decompose-router/src/androidMain/kotlin/io/github/xxfast/decompose/router/Key.kt index 7980f78..277b122 100644 --- a/decompose-router/src/androidMain/kotlin/io/github/xxfast/decompose/router/Key.kt +++ b/decompose-router/src/androidMain/kotlin/io/github/xxfast/decompose/router/Key.kt @@ -2,5 +2,5 @@ package io.github.xxfast.decompose.router import kotlin.reflect.KClass -internal actual val KClass<*>.key: String get() = +actual val KClass<*>.key: String get() = requireNotNull(qualifiedName) { "Unable to use name of $this as the default key"} diff --git a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/Key.kt b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/Key.kt index df6b7cf..7cd7fd1 100644 --- a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/Key.kt +++ b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/Key.kt @@ -2,4 +2,4 @@ package io.github.xxfast.decompose.router import kotlin.reflect.KClass -internal expect val KClass<*>.key: String +expect val KClass<*>.key: String 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 48b9aa7..14c6c90 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 @@ -17,8 +17,8 @@ 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 kotlinx.serialization.* import kotlin.reflect.KClass /*** @@ -28,7 +28,7 @@ import kotlin.reflect.KClass * @param navigation decompose navigator to use * @param stack state of decompose child stack to use */ -class Router( +class Router( private val navigation: StackNavigation, val stack: State>, ) : StackNavigation by navigation @@ -39,6 +39,14 @@ class Router( val LocalRouter: ProvidableCompositionLocal?> = staticCompositionLocalOf { null } +// TODO: Add this back to API once this [issue](https://github.com/JetBrains/compose-multiplatform/issues/2900) is fixed +//@Composable +//inline fun rememberRouter( +// key: Any = C::class, +// handleBackButton: Boolean = true, +// noinline initialStack: () -> List, +//): Router = rememberRouter(C::class, key, handleBackButton, initialStack) + /*** * Creates a router that retains a stack of [C] configuration * @@ -47,8 +55,9 @@ val LocalRouter: ProvidableCompositionLocal?> = * @param initialStack initial stack of configurations * @param handleBackButton should the router handle back button */ +@OptIn(InternalSerializationApi::class) @Composable -fun rememberRouter( +fun rememberRouter( type: KClass, key: Any = type.key, handleBackButton: Boolean = true, @@ -63,8 +72,8 @@ fun rememberRouter( val stack: State> = routerContext .childStack( source = navigation, + serializer = type.serializerOrNull(), initialStack = initialStack, - configurationClass = type, key = routerKey, handleBackButton = handleBackButton, childFactory = { _, childComponentContext -> RouterContext(childComponentContext) }, @@ -76,23 +85,23 @@ fun rememberRouter( } } -private fun Value.asState(lifecycle: Lifecycle): State { +fun Value.asState(lifecycle: Lifecycle): State { val state = mutableStateOf(value) observe(lifecycle = lifecycle) { state.value = it } return state } /*** - * Creates a instance of [T] that is scoped to the current route + * Creates an instance of [T] that is scoped to the current route * * @param type class of [T] instance * @param key key to remember the instance with. Defaults to [type]'s key * @param block lambda to create an instance of [T] with a given [SavedStateHandle] */ -@Suppress("UNCHECKED_CAST") @Composable -fun rememberOnRoute( +fun rememberOnRoute( type: KClass, + strategy: KSerializer, key: Any = type.key, block: @DisallowComposableCalls (savedState: SavedStateHandle) -> T ): T { @@ -103,7 +112,7 @@ fun rememberOnRoute( val stateKey = "$key.state" val (instance, savedState) = remember(key) { val savedState: SavedStateHandle = instanceKeeper - .getOrCreate(stateKey) { SavedStateHandle(stateKeeper.consume(stateKey, SavedState::class)) } + .getOrCreate(stateKey) { SavedStateHandle(stateKeeper.consume(stateKey, strategy)) } var instance: T? = instanceKeeper.get(instanceKey) as T? if (instance == null) { instance = block(savedState) @@ -114,7 +123,7 @@ fun rememberOnRoute( LaunchedEffect(Unit) { if (!stateKeeper.isRegistered(stateKey)) - stateKeeper.register(stateKey) { savedState.value } + stateKeeper.register(stateKey, strategy) { savedState.value as C? } } return 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 index fa7f065..fb18d85 100644 --- 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 @@ -9,7 +9,7 @@ import com.arkivanov.essenty.instancekeeper.InstanceKeeper import com.arkivanov.essenty.lifecycle.Lifecycle import com.arkivanov.essenty.statekeeper.StateKeeper -class RouterContext internal constructor( +class RouterContext( private val delegate: ComponentContext, ) : ComponentContext by delegate { @@ -20,10 +20,10 @@ class RouterContext internal constructor( backHandler: BackHandler? = null, ) : this(DefaultComponentContext(lifecycle, stateKeeper, instanceKeeper, backHandler)) - internal val storage: MutableMap = HashMap() + val storage: MutableMap = HashMap() } -internal inline fun RouterContext.getOrCreate(key: Any, factory: () -> T) : T { +inline fun RouterContext.getOrCreate(key: Any, factory: () -> T) : T { var instance: T? = storage[key] as T? if (instance == null) { instance = factory() diff --git a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/SavedState.kt b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/SavedState.kt index 17d8011..27344c9 100644 --- a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/SavedState.kt +++ b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/SavedState.kt @@ -1,25 +1,15 @@ package io.github.xxfast.decompose.router import com.arkivanov.essenty.instancekeeper.InstanceKeeper -import com.arkivanov.essenty.parcelable.Parcelable -import com.arkivanov.essenty.parcelable.Parcelize - -/*** - * A wrapper to retain [Parcelable] across process death - * - * @param value parcelable to retain - */ -@Parcelize -data class SavedState(val value: Parcelable): Parcelable +import kotlinx.serialization.Serializable /*** * Handle to help the view models manage saved state */ -@Suppress("UNCHECKED_CAST") // I know what i'm doing -class SavedStateHandle(default: SavedState?): InstanceKeeper.Instance { - private var savedState: SavedState? = default - val value: Parcelable? get() = savedState - fun get(): T? = savedState?.value as? T? - fun set(value: Parcelable) { this.savedState = SavedState(value) } +class SavedStateHandle(default: @Serializable Any?): InstanceKeeper.Instance { + private var savedState: @Serializable Any? = default + val value: @Serializable Any? get() = savedState + fun get(): T? = savedState as? T + fun set(value: @Serializable Any) { this.savedState = value } override fun onDestroy() { savedState = null } } 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 5f10ed7..72a7baa 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 @@ -10,6 +10,7 @@ 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 +import kotlinx.serialization.Serializable /*** * Composable to hoist content that are navigated by the router @@ -20,7 +21,7 @@ import io.github.xxfast.decompose.router.RouterContext * @param content */ @Composable -fun RoutedContent( +fun RoutedContent( router: Router, modifier: Modifier = Modifier, animation: StackAnimation? = null, diff --git a/decompose-router/src/desktopMain/kotlin/io/github/xxfast/decompose/router/Key.kt b/decompose-router/src/desktopMain/kotlin/io/github/xxfast/decompose/router/Key.kt index 7980f78..277b122 100644 --- a/decompose-router/src/desktopMain/kotlin/io/github/xxfast/decompose/router/Key.kt +++ b/decompose-router/src/desktopMain/kotlin/io/github/xxfast/decompose/router/Key.kt @@ -2,5 +2,5 @@ package io.github.xxfast.decompose.router import kotlin.reflect.KClass -internal actual val KClass<*>.key: String get() = +actual val KClass<*>.key: String get() = requireNotNull(qualifiedName) { "Unable to use name of $this as the default key"} diff --git a/decompose-router/src/iosMain/kotlin/io/github/xxfast/decompose/router/Key.kt b/decompose-router/src/iosMain/kotlin/io/github/xxfast/decompose/router/Key.kt index 7980f78..277b122 100644 --- a/decompose-router/src/iosMain/kotlin/io/github/xxfast/decompose/router/Key.kt +++ b/decompose-router/src/iosMain/kotlin/io/github/xxfast/decompose/router/Key.kt @@ -2,5 +2,5 @@ package io.github.xxfast.decompose.router import kotlin.reflect.KClass -internal actual val KClass<*>.key: String get() = +actual val KClass<*>.key: String get() = requireNotNull(qualifiedName) { "Unable to use name of $this as the default key"} diff --git a/decompose-router/src/jsMain/kotlin/io/github/xxfast/decompose/router/Key.kt b/decompose-router/src/jsMain/kotlin/io/github/xxfast/decompose/router/Key.kt index 45a7076..2ec0c5a 100644 --- a/decompose-router/src/jsMain/kotlin/io/github/xxfast/decompose/router/Key.kt +++ b/decompose-router/src/jsMain/kotlin/io/github/xxfast/decompose/router/Key.kt @@ -3,5 +3,5 @@ package io.github.xxfast.decompose.router import kotlin.reflect.KClass // TODO: Given that we don't have tree-shaking on js - yet, should be safe to use simpleName here -internal actual val KClass<*>.key: String get() = +actual val KClass<*>.key: String get() = requireNotNull(simpleName) { "Unable to use name of $this as the default key"} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4fb9a46..9765763 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,6 @@ compose-ui-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose-test-rule" } decompose = { module = "com.arkivanov.decompose:decompose", version.ref = "decompose" } decompose-compose-multiplatform = { module = "com.arkivanov.decompose:extensions-compose-jetbrains", version.ref = "decompose" } -essenty-parcelable = { module = "com.arkivanov.essenty:parcelable", version.ref = "essenty" } horologist-compose-layouts = { module = "com.google.android.horologist:horologist-compose-layout", version.ref = "horologist" } kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }