Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Made StateKeeper#saveable type nullable [cherry-pick to v2.3.0] #191

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions state-keeper/api/state-keeper.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -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§<kotlin.Any>}[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§<kotlin.Any>}[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§<kotlin.Any>}[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<kotlin/Any?, kotlin.properties/ReadOnlyProperty<kotlin/Any?, #A>> // com.arkivanov.essenty.statekeeper/saveable|[email protected](kotlinx.serialization.KSerializer<0:1>;kotlin.Function1<0:0,0:1>;kotlin.String?;kotlin.Function1<0:1?,0:0>){0§<kotlin.Any?>;1§<kotlin.Any?>}[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<kotlin/Any?, kotlin.properties/ReadOnlyProperty<kotlin/Any?, #A>> // com.arkivanov.essenty.statekeeper/saveable|[email protected](kotlinx.serialization.KSerializer<0:1>;kotlin.Function1<0:0,0:1>;kotlin.String?;kotlin.Function1<0:1?,0:0>){0§<kotlin.Any?>;1§<kotlin.Any?>}[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<kotlin/Any?, kotlin.properties/ReadWriteProperty<kotlin/Any?, #A>> // com.arkivanov.essenty.statekeeper/saveable|[email protected](kotlinx.serialization.KSerializer<0:0>;kotlin.String?;kotlin.Function0<0:0>){0§<kotlin.Any?>}[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<kotlin/Any?, kotlin.properties/ReadWriteProperty<kotlin/Any?, #A>> // com.arkivanov.essenty.statekeeper/saveable|[email protected](kotlinx.serialization.KSerializer<0:0>;kotlin.String?;kotlin.Function0<0:0>){0§<kotlin.Any?>}[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<kotlin/Any?, kotlin.properties/ReadWriteProperty<kotlin/Any?, #A>> // com.arkivanov.essenty.statekeeper/saveable|[email protected](kotlinx.serialization.KSerializer<0:0>;kotlin.String?;kotlin.Function0<0:0>){0§<kotlin.Any>}[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<kotlin/Any?, kotlin.properties/ReadWriteProperty<kotlin/Any?, #A>> // com.arkivanov.essenty.statekeeper/saveable|[email protected](kotlinx.serialization.KSerializer<0:0>;kotlin.String?;kotlin.Function0<0:0>){0§<kotlin.Any>}[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<kotlin/Any?, kotlin.properties/ReadOnlyProperty<kotlin/Any?, #A>> // com.arkivanov.essenty.statekeeper/saveable|[email protected](kotlinx.serialization.KSerializer<0:1>;kotlin.Function1<0:0,0:1>;kotlin.String?;kotlin.Function1<0:1?,0:0>){0§<kotlin.Any?>;1§<kotlin.Any>}[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<kotlin/Any?, kotlin.properties/ReadOnlyProperty<kotlin/Any?, #A>> // com.arkivanov.essenty.statekeeper/saveable|[email protected](kotlinx.serialization.KSerializer<0:1>;kotlin.Function1<0:0,0:1>;kotlin.String?;kotlin.Function1<0:1?,0:0>){0§<kotlin.Any?>;1§<kotlin.Any>}[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§<kotlin.Any>}[0]
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
Expand All @@ -21,16 +46,17 @@ import kotlin.reflect.KProperty
* @return [PropertyDelegateProvider] of type [T], typically used to define a delegated property.
*/
@ExperimentalStateKeeperApi
inline fun <T, S : Any> StateKeeper.saveable(
fun <T, S> StateKeeper.saveable(
serializer: KSerializer<S>,
crossinline state: (T) -> S,
state: (T) -> S,
key: String? = null,
crossinline init: (savedState: S?) -> T,
init: (savedState: S?) -> T,
): PropertyDelegateProvider<Any?, ReadOnlyProperty<Any?, T>> =
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 }
}

Expand All @@ -39,6 +65,29 @@ inline fun <T, S : Any> 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.
Expand All @@ -49,11 +98,11 @@ inline fun <T, S : Any> StateKeeper.saveable(
* @return [PropertyDelegateProvider] of type [T], typically used to define a delegated property.
*/
@ExperimentalStateKeeperApi
inline fun <T, S : Any> StateKeeperOwner.saveable(
fun <T, S> StateKeeperOwner.saveable(
serializer: KSerializer<S>,
crossinline state: (T) -> S,
state: (T) -> S,
key: String? = null,
crossinline init: (savedState: S?) -> T,
init: (savedState: S?) -> T,
): PropertyDelegateProvider<Any?, ReadOnlyProperty<Any?, T>> =
stateKeeper.saveable(
serializer = serializer,
Expand All @@ -67,49 +116,82 @@ inline fun <T, S : Any> 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.
* @param init a function returning the initial value of type [T].
* @return [PropertyDelegateProvider] of type [T], typically used to define a delegated property.
*/
@ExperimentalStateKeeperApi
inline fun <T : Any> StateKeeper.saveable(
fun <T> StateKeeper.saveable(
serializer: KSerializer<T>,
key: String? = null,
crossinline init: () -> T,
init: () -> T,
): PropertyDelegateProvider<Any?, ReadWriteProperty<Any?, T>> =
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<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T =
saveable
@Serializable
private class Holder<T>(@Volatile var value: T) : ReadWriteProperty<Any?, T> {
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.
* @param init a function returning the initial value of type [T].
* @return [PropertyDelegateProvider] of type [T], typically used to define a delegated property.
*/
@ExperimentalStateKeeperApi
inline fun <T : Any> StateKeeperOwner.saveable(
fun <T> StateKeeperOwner.saveable(
serializer: KSerializer<T>,
key: String? = null,
crossinline init: () -> T,
init: () -> T,
): PropertyDelegateProvider<Any?, ReadWriteProperty<Any?, T>> =
stateKeeper.saveable(
serializer = serializer,
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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()
Expand All @@ -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?)
}
Loading