Skip to content

Commit

Permalink
Migrate from parcelable and parcelize to kotlinx serializable
Browse files Browse the repository at this point in the history
  • Loading branch information
xxfast committed Nov 30, 2023
1 parent d0a560e commit 8e04963
Show file tree
Hide file tree
Showing 19 changed files with 59 additions and 61 deletions.
3 changes: 1 addition & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -70,7 +70,6 @@ kotlin {

implementation(libs.decompose)
implementation(libs.decompose.compose.multiplatform)
implementation(libs.essenty.parcelable)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}

Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Int>? = Loading): Parcelable
@Serializable data class ListState(val items: List<Int>? = Loading)

val Loading: Nothing? = null
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ buildscript {
classpath(libs.agp)
classpath(libs.compose.multiplatform)
classpath(libs.kotlin.gradle.plugin)
classpath(libs.kotlinx.serialization)
}
}

Expand Down
1 change: 0 additions & 1 deletion decompose-router-wear/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion decompose-router/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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

/***
Expand All @@ -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<C : Parcelable>(
class Router<C: Any>(
private val navigation: StackNavigation<C>,
val stack: State<ChildStack<C, RouterContext>>,
) : StackNavigation<C> by navigation
Expand All @@ -39,6 +39,14 @@ class Router<C : Parcelable>(
val LocalRouter: ProvidableCompositionLocal<Router<*>?> =
staticCompositionLocalOf { null }

// TODO: Add this back to API once this [issue](https://github.com/JetBrains/compose-multiplatform/issues/2900) is fixed
//@Composable
//inline fun <reified C: @Serializable Any> rememberRouter(
// key: Any = C::class,
// handleBackButton: Boolean = true,
// noinline initialStack: () -> List<C>,
//): Router<C> = rememberRouter(C::class, key, handleBackButton, initialStack)

/***
* Creates a router that retains a stack of [C] configuration
*
Expand All @@ -47,8 +55,9 @@ val LocalRouter: ProvidableCompositionLocal<Router<*>?> =
* @param initialStack initial stack of configurations
* @param handleBackButton should the router handle back button
*/
@OptIn(InternalSerializationApi::class)
@Composable
fun <C : Parcelable> rememberRouter(
fun <C: @Serializable Any> rememberRouter(
type: KClass<C>,
key: Any = type.key,
handleBackButton: Boolean = true,
Expand All @@ -63,8 +72,8 @@ fun <C : Parcelable> rememberRouter(
val stack: State<ChildStack<C, RouterContext>> = routerContext
.childStack(
source = navigation,
serializer = type.serializerOrNull(),
initialStack = initialStack,
configurationClass = type,
key = routerKey,
handleBackButton = handleBackButton,
childFactory = { _, childComponentContext -> RouterContext(childComponentContext) },
Expand All @@ -76,23 +85,23 @@ fun <C : Parcelable> rememberRouter(
}
}

private fun <T : Any> Value<T>.asState(lifecycle: Lifecycle): State<T> {
fun <T : Any> Value<T>.asState(lifecycle: Lifecycle): State<T> {
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 <T : Instance> rememberOnRoute(
fun <T : Instance, C: @Serializable Any> rememberOnRoute(
type: KClass<T>,
strategy: KSerializer<C>,
key: Any = type.key,
block: @DisallowComposableCalls (savedState: SavedStateHandle) -> T
): T {
Expand All @@ -103,7 +112,7 @@ fun <T : Instance> 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)
Expand All @@ -114,7 +123,7 @@ fun <T : Instance> rememberOnRoute(

LaunchedEffect(Unit) {
if (!stateKeeper.isRegistered(stateKey))
stateKeeper.register(stateKey) { savedState.value }
stateKeeper.register(stateKey, strategy) { savedState.value as C? }
}

return instance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -20,10 +20,10 @@ class RouterContext internal constructor(
backHandler: BackHandler? = null,
) : this(DefaultComponentContext(lifecycle, stateKeeper, instanceKeeper, backHandler))

internal val storage: MutableMap<Any, Any> = HashMap()
val storage: MutableMap<Any, Any> = HashMap()
}

internal inline fun <reified T : Any> RouterContext.getOrCreate(key: Any, factory: () -> T) : T {
inline fun <reified T : Any> RouterContext.getOrCreate(key: Any, factory: () -> T) : T {
var instance: T? = storage[key] as T?
if (instance == null) {
instance = factory()
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <T: Parcelable> 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 <T: @Serializable Any> get(): T? = savedState as? T
fun set(value: @Serializable Any) { this.savedState = value }
override fun onDestroy() { savedState = null }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,7 +21,7 @@ import io.github.xxfast.decompose.router.RouterContext
* @param content
*/
@Composable
fun <C : Parcelable> RoutedContent(
fun <C: @Serializable Any> RoutedContent(
router: Router<C>,
modifier: Modifier = Modifier,
animation: StackAnimation<C, RouterContext>? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
1 change: 0 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down

0 comments on commit 8e04963

Please sign in to comment.