diff --git a/state-keeper/api/state-keeper.klib.api b/state-keeper/api/state-keeper.klib.api index 2b70f2b..202f9d0 100644 --- a/state-keeper/api/state-keeper.klib.api +++ b/state-keeper/api/state-keeper.klib.api @@ -41,9 +41,9 @@ final class com.arkivanov.essenty.statekeeper/SerializableContainer { // com.ark final fun <#A: kotlin/Any> (com.arkivanov.essenty.statekeeper/SerializableContainer).com.arkivanov.essenty.statekeeper/consumeRequired(kotlinx.serialization/DeserializationStrategy<#A>): #A // com.arkivanov.essenty.statekeeper/consumeRequired|consumeRequired@com.arkivanov.essenty.statekeeper.SerializableContainer(kotlinx.serialization.DeserializationStrategy<0:0>){0§}[0] final fun <#A: kotlin/Any> com.arkivanov.essenty.statekeeper/SerializableContainer(#A?, kotlinx.serialization/SerializationStrategy<#A>): com.arkivanov.essenty.statekeeper/SerializableContainer // com.arkivanov.essenty.statekeeper/SerializableContainer|SerializableContainer(0:0?;kotlinx.serialization.SerializationStrategy<0:0>){0§}[0] final fun <#A: kotlin/Any> com.arkivanov.essenty.statekeeper/polymorphicSerializer(kotlin.reflect/KClass<#A>, kotlinx.serialization.modules/SerializersModule): kotlinx.serialization/KSerializer<#A> // com.arkivanov.essenty.statekeeper/polymorphicSerializer|polymorphicSerializer(kotlin.reflect.KClass<0:0>;kotlinx.serialization.modules.SerializersModule){0§}[0] +final fun <#A: kotlin/Any?, #B: kotlin/Any?> (com.arkivanov.essenty.statekeeper/StateKeeper).com.arkivanov.essenty.statekeeper/saveable(kotlinx.serialization/KSerializer<#B>, kotlin/Function1<#A, #B>, kotlin/String? = ..., kotlin/Function1<#B?, #A>): kotlin.properties/PropertyDelegateProvider> // com.arkivanov.essenty.statekeeper/saveable|saveable@com.arkivanov.essenty.statekeeper.StateKeeper(kotlinx.serialization.KSerializer<0:1>;kotlin.Function1<0:0,0:1>;kotlin.String?;kotlin.Function1<0:1?,0:0>){0§;1§}[0] +final fun <#A: kotlin/Any?, #B: kotlin/Any?> (com.arkivanov.essenty.statekeeper/StateKeeperOwner).com.arkivanov.essenty.statekeeper/saveable(kotlinx.serialization/KSerializer<#B>, kotlin/Function1<#A, #B>, kotlin/String? = ..., kotlin/Function1<#B?, #A>): kotlin.properties/PropertyDelegateProvider> // com.arkivanov.essenty.statekeeper/saveable|saveable@com.arkivanov.essenty.statekeeper.StateKeeperOwner(kotlinx.serialization.KSerializer<0:1>;kotlin.Function1<0:0,0:1>;kotlin.String?;kotlin.Function1<0:1?,0:0>){0§;1§}[0] +final fun <#A: kotlin/Any?> (com.arkivanov.essenty.statekeeper/StateKeeper).com.arkivanov.essenty.statekeeper/saveable(kotlinx.serialization/KSerializer<#A>, kotlin/String? = ..., kotlin/Function0<#A>): kotlin.properties/PropertyDelegateProvider> // com.arkivanov.essenty.statekeeper/saveable|saveable@com.arkivanov.essenty.statekeeper.StateKeeper(kotlinx.serialization.KSerializer<0:0>;kotlin.String?;kotlin.Function0<0:0>){0§}[0] +final fun <#A: kotlin/Any?> (com.arkivanov.essenty.statekeeper/StateKeeperOwner).com.arkivanov.essenty.statekeeper/saveable(kotlinx.serialization/KSerializer<#A>, kotlin/String? = ..., kotlin/Function0<#A>): kotlin.properties/PropertyDelegateProvider> // com.arkivanov.essenty.statekeeper/saveable|saveable@com.arkivanov.essenty.statekeeper.StateKeeperOwner(kotlinx.serialization.KSerializer<0:0>;kotlin.String?;kotlin.Function0<0:0>){0§}[0] final fun com.arkivanov.essenty.statekeeper/StateKeeperDispatcher(com.arkivanov.essenty.statekeeper/SerializableContainer? = ...): com.arkivanov.essenty.statekeeper/StateKeeperDispatcher // com.arkivanov.essenty.statekeeper/StateKeeperDispatcher|StateKeeperDispatcher(com.arkivanov.essenty.statekeeper.SerializableContainer?){}[0] -final inline fun <#A: kotlin/Any> (com.arkivanov.essenty.statekeeper/StateKeeper).com.arkivanov.essenty.statekeeper/saveable(kotlinx.serialization/KSerializer<#A>, kotlin/String? = ..., crossinline kotlin/Function0<#A>): kotlin.properties/PropertyDelegateProvider> // com.arkivanov.essenty.statekeeper/saveable|saveable@com.arkivanov.essenty.statekeeper.StateKeeper(kotlinx.serialization.KSerializer<0:0>;kotlin.String?;kotlin.Function0<0:0>){0§}[0] -final inline fun <#A: kotlin/Any> (com.arkivanov.essenty.statekeeper/StateKeeperOwner).com.arkivanov.essenty.statekeeper/saveable(kotlinx.serialization/KSerializer<#A>, kotlin/String? = ..., crossinline kotlin/Function0<#A>): kotlin.properties/PropertyDelegateProvider> // com.arkivanov.essenty.statekeeper/saveable|saveable@com.arkivanov.essenty.statekeeper.StateKeeperOwner(kotlinx.serialization.KSerializer<0:0>;kotlin.String?;kotlin.Function0<0:0>){0§}[0] -final inline fun <#A: kotlin/Any?, #B: kotlin/Any> (com.arkivanov.essenty.statekeeper/StateKeeper).com.arkivanov.essenty.statekeeper/saveable(kotlinx.serialization/KSerializer<#B>, crossinline kotlin/Function1<#A, #B>, kotlin/String? = ..., crossinline kotlin/Function1<#B?, #A>): kotlin.properties/PropertyDelegateProvider> // com.arkivanov.essenty.statekeeper/saveable|saveable@com.arkivanov.essenty.statekeeper.StateKeeper(kotlinx.serialization.KSerializer<0:1>;kotlin.Function1<0:0,0:1>;kotlin.String?;kotlin.Function1<0:1?,0:0>){0§;1§}[0] -final inline fun <#A: kotlin/Any?, #B: kotlin/Any> (com.arkivanov.essenty.statekeeper/StateKeeperOwner).com.arkivanov.essenty.statekeeper/saveable(kotlinx.serialization/KSerializer<#B>, crossinline kotlin/Function1<#A, #B>, kotlin/String? = ..., crossinline kotlin/Function1<#B?, #A>): kotlin.properties/PropertyDelegateProvider> // com.arkivanov.essenty.statekeeper/saveable|saveable@com.arkivanov.essenty.statekeeper.StateKeeperOwner(kotlinx.serialization.KSerializer<0:1>;kotlin.Function1<0:0,0:1>;kotlin.String?;kotlin.Function1<0:1?,0:0>){0§;1§}[0] final inline fun <#A: reified kotlin/Any> com.arkivanov.essenty.statekeeper/polymorphicSerializer(kotlinx.serialization.modules/SerializersModule): kotlinx.serialization/KSerializer<#A> // com.arkivanov.essenty.statekeeper/polymorphicSerializer|polymorphicSerializer(kotlinx.serialization.modules.SerializersModule){0§}[0] diff --git a/state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/StateKeeperExt.kt b/state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/StateKeeperExt.kt index 89bd3ad..b40cb17 100644 --- a/state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/StateKeeperExt.kt +++ b/state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/StateKeeperExt.kt @@ -1,6 +1,8 @@ package com.arkivanov.essenty.statekeeper import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlin.concurrent.Volatile import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadWriteProperty @@ -11,6 +13,29 @@ import kotlin.reflect.KProperty * [delegated property](https://kotlinlang.org/docs/delegated-properties.html) holding an object * whose state is automatically saved and restored using [StateKeeper]. * + * Example: + * + * ``` + * import com.arkivanov.essenty.statekeeper.StateKeeper + * import com.arkivanov.essenty.statekeeper.saveable + * import kotlinx.serialization.Serializable + * + * private class SomeLogic(stateKeeper: StateKeeper) { + * + * private val stateHolder by stateKeeper.saveable( + * serializer = State.serializer(), + * state = StateHolder::state, + * ) { savedState -> + * StateHolder(state = savedState ?: State()) + * } + * + * private class StateHolder(var state: State) + * + * @Serializable + * private class State(val someValue: Int = 0) + * } + * ``` + * * @param serializer a [KSerializer] for serializing and deserializing the state. * @param state a function that selects a state [S] from the resulting * object [T] and returns it for saving. @@ -21,16 +46,17 @@ import kotlin.reflect.KProperty * @return [PropertyDelegateProvider] of type [T], typically used to define a delegated property. */ @ExperimentalStateKeeperApi -inline fun StateKeeper.saveable( +fun StateKeeper.saveable( serializer: KSerializer, - crossinline state: (T) -> S, + state: (T) -> S, key: String? = null, - crossinline init: (savedState: S?) -> T, + init: (savedState: S?) -> T, ): PropertyDelegateProvider> = PropertyDelegateProvider { _, property -> val stateKey = key ?: "SAVEABLE_HOLDER_${property.name}" - val result = init(consume(key = stateKey, strategy = serializer)) - register(key = stateKey, strategy = serializer) { state(result) } + val holderSerializer = Holder.serializer(serializer) + val result = init(consume(key = stateKey, strategy = holderSerializer)?.value) + register(key = stateKey, strategy = holderSerializer) { Holder(state(result)) } ReadOnlyProperty { _, _ -> result } } @@ -39,6 +65,29 @@ inline fun StateKeeper.saveable( * [delegated property](https://kotlinlang.org/docs/delegated-properties.html) holding an object * whose state is automatically saved and restored using [StateKeeper]. * + * Example: + * + * ``` + * import com.arkivanov.essenty.statekeeper.StateKeeper + * import com.arkivanov.essenty.statekeeper.saveable + * import kotlinx.serialization.Serializable + * + * private class SomeLogic(override val stateKeeper: StateKeeper) : StateKeeperOwner { + * + * private val stateHolder by saveable( + * serializer = State.serializer(), + * state = StateHolder::state, + * ) { savedState -> + * StateHolder(state = savedState ?: State()) + * } + * + * private class StateHolder(var state: State) + * + * @Serializable + * private class State(val someValue: Int = 0) + * } + * ``` + * * @param serializer a [KSerializer] for serializing and deserializing the state. * @param state a function that selects a state [S] from the resulting * object [T] and returns it for saving. @@ -49,11 +98,11 @@ inline fun StateKeeper.saveable( * @return [PropertyDelegateProvider] of type [T], typically used to define a delegated property. */ @ExperimentalStateKeeperApi -inline fun StateKeeperOwner.saveable( +fun StateKeeperOwner.saveable( serializer: KSerializer, - crossinline state: (T) -> S, + state: (T) -> S, key: String? = null, - crossinline init: (savedState: S?) -> T, + init: (savedState: S?) -> T, ): PropertyDelegateProvider> = stateKeeper.saveable( serializer = serializer, @@ -67,6 +116,21 @@ inline fun StateKeeperOwner.saveable( * [delegated property](https://kotlinlang.org/docs/delegated-properties.html) whose value is * automatically saved and restored using [StateKeeper]. * + * Example: + * + * ``` + * import com.arkivanov.essenty.statekeeper.StateKeeper + * import com.arkivanov.essenty.statekeeper.saveable + * import kotlinx.serialization.Serializable + * + * class SomeLogic(stateKeeper: StateKeeper) { + * private var state: State by stateKeeper.saveable(serializer = State.serializer(), init = ::State) + * + * @Serializable + * private class State(val someValue: Int = 0) + * } + * ``` + * * @param serializer a [KSerializer] for serializing and deserializing values of type [T]. * @param key an optional key for saving and restoring the value. If not provided, then the * property name is used as a key. @@ -74,31 +138,49 @@ inline fun StateKeeperOwner.saveable( * @return [PropertyDelegateProvider] of type [T], typically used to define a delegated property. */ @ExperimentalStateKeeperApi -inline fun StateKeeper.saveable( +fun StateKeeper.saveable( serializer: KSerializer, key: String? = null, - crossinline init: () -> T, + init: () -> T, ): PropertyDelegateProvider> = PropertyDelegateProvider { _, property -> val stateKey = key ?: "SAVEABLE_${property.name}" - var saveable = consume(key = stateKey, strategy = serializer) ?: init() - register(key = stateKey, strategy = serializer) { saveable } + val holderSerializer = Holder.serializer(serializer) + val holder = consume(key = stateKey, strategy = holderSerializer) ?: Holder(init()) + register(key = stateKey, strategy = holderSerializer) { Holder(holder.value) } + holder + } - object : ReadWriteProperty { - override fun getValue(thisRef: Any?, property: KProperty<*>): T = - saveable +@Serializable +private class Holder(@Volatile var value: T) : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): T = + this.value - override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { - saveable = value - } - } + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + this.value = value } +} /** * Helper function for creating a mutable * [delegated property]((https://kotlinlang.org/docs/delegated-properties.html)) whose value is * automatically saved and restored using [StateKeeper]. * + * Example: + * + * ``` + * import com.arkivanov.essenty.statekeeper.StateKeeper + * import com.arkivanov.essenty.statekeeper.saveable + * import kotlinx.serialization.Serializable + * + * class SomeLogic(override val stateKeeper: StateKeeper) : StateKeeperOwner { + * private var state: State by saveable(serializer = State.serializer(), init = ::State) + * + * @Serializable + * private class State(val someValue: Int = 0) + * } + * ``` + * * @param serializer a [KSerializer] for serializing and deserializing values of type [T]. * @param key an optional key for saving and restoring the value. If not provided, then the * property name is used as a key. @@ -106,10 +188,10 @@ inline fun StateKeeper.saveable( * @return [PropertyDelegateProvider] of type [T], typically used to define a delegated property. */ @ExperimentalStateKeeperApi -inline fun StateKeeperOwner.saveable( +fun StateKeeperOwner.saveable( serializer: KSerializer, key: String? = null, - crossinline init: () -> T, + init: () -> T, ): PropertyDelegateProvider> = stateKeeper.saveable( serializer = serializer, diff --git a/state-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/StateKeeperExtTest.kt b/state-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/StateKeeperExtTest.kt index 429df48..4d04ad7 100644 --- a/state-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/StateKeeperExtTest.kt +++ b/state-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/StateKeeperExtTest.kt @@ -1,8 +1,10 @@ package com.arkivanov.essenty.statekeeper +import kotlinx.serialization.builtins.nullable import kotlinx.serialization.builtins.serializer import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNull @OptIn(ExperimentalStateKeeperApi::class) class StateKeeperExtTest { @@ -21,6 +23,20 @@ class StateKeeperExtTest { assertEquals(1, newComponent.holder.state) } + @Test + fun saveable_holder_saves_and_restores_nullable_state() { + val oldStateKeeper = StateKeeperDispatcher() + val oldComponent = ComponentWithStateHolder(oldStateKeeper) + + oldComponent.nullableHolder.state = 1 + + val savedState = oldStateKeeper.save().serializeAndDeserialize() + val newStateKeeper = StateKeeperDispatcher(savedState = savedState) + val newComponent = ComponentWithStateHolder(newStateKeeper) + + assertEquals(1, newComponent.nullableHolder.state) + } + @Test fun saveable_property_saves_and_restores_state() { val oldStateKeeper = StateKeeperDispatcher() @@ -35,15 +51,50 @@ class StateKeeperExtTest { assertEquals(1, newComponent.state) } - private class ComponentWithStateHolder(stateKeeper: StateKeeper) { - val holder by stateKeeper.saveable(serializer = Int.serializer(), state = Holder::state) { + @Test + fun saveable_property_saves_and_restores_nullable_state_1() { + val oldStateKeeper = StateKeeperDispatcher() + val oldComponent = ComponentWithState(oldStateKeeper) + + oldComponent.nullableState1 = null + + val savedState = oldStateKeeper.save().serializeAndDeserialize() + val newStateKeeper = StateKeeperDispatcher(savedState = savedState) + val newComponent = ComponentWithState(newStateKeeper) + + assertNull(newComponent.nullableState1) + } + + @Test + fun saveable_property_saves_and_restores_nullable_state_2() { + val oldStateKeeper = StateKeeperDispatcher() + val oldComponent = ComponentWithState(oldStateKeeper) + + oldComponent.nullableState2 = 1 + + val savedState = oldStateKeeper.save().serializeAndDeserialize() + val newStateKeeper = StateKeeperDispatcher(savedState = savedState) + val newComponent = ComponentWithState(newStateKeeper) + + assertEquals(1, newComponent.nullableState2) + } + + private class ComponentWithStateHolder(override val stateKeeper: StateKeeper) : StateKeeperOwner { + val holder by saveable(serializer = Int.serializer(), state = Holder::state) { Holder(state = it ?: 0) } + + val nullableHolder by saveable(serializer = Int.serializer().nullable, state = NullableHolder::state) { + NullableHolder(state = it) + } } - private class ComponentWithState(stateKeeper: StateKeeper) { - var state by stateKeeper.saveable(serializer = Int.serializer()) { 0 } + private class ComponentWithState(override val stateKeeper: StateKeeper) : StateKeeperOwner { + var state: Int by saveable(serializer = Int.serializer()) { 0 } + var nullableState1: Int? by saveable(serializer = Int.serializer().nullable) { 0 } + var nullableState2: Int? by saveable(serializer = Int.serializer().nullable) { null } } private class Holder(var state: Int) + private class NullableHolder(var state: Int?) }