Skip to content

Commit a8aff61

Browse files
authored
Fix SaveableMutableStateSource (#367)
- It would save, but updates after restoring would get lost - Also added a SerializableMutableStateSource
1 parent 8255f00 commit a8aff61

File tree

9 files changed

+483
-11
lines changed

9 files changed

+483
-11
lines changed

vice-sources/api/android/vice-sources.api

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,16 @@ public abstract class com/eygraber/vice/sources/SaveableMutableStateSource : com
100100
protected final fun update (Ljava/lang/Object;)V
101101
}
102102

103+
public abstract class com/eygraber/vice/sources/SerializableMutableStateSource : com/eygraber/vice/sources/StateSource {
104+
public static final field $stable I
105+
public fun <init> (Lkotlinx/serialization/KSerializer;)V
106+
public fun currentState (Landroidx/compose/runtime/Composer;I)Ljava/lang/Object;
107+
protected abstract fun getInitial ()Ljava/lang/Object;
108+
public final fun getUpdates ()Lkotlinx/coroutines/flow/Flow;
109+
public fun getValue ()Ljava/lang/Object;
110+
protected final fun update (Ljava/lang/Object;)V
111+
}
112+
103113
public abstract class com/eygraber/vice/sources/StateFlowSource : com/eygraber/vice/ViceSource {
104114
public static final field $stable I
105115
public fun <init> ()V

vice-sources/api/jvm/vice-sources.api

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,16 @@ public abstract class com/eygraber/vice/sources/SaveableMutableStateSource : com
100100
protected final fun update (Ljava/lang/Object;)V
101101
}
102102

103+
public abstract class com/eygraber/vice/sources/SerializableMutableStateSource : com/eygraber/vice/sources/StateSource {
104+
public static final field $stable I
105+
public fun <init> (Lkotlinx/serialization/KSerializer;)V
106+
public fun currentState (Landroidx/compose/runtime/Composer;I)Ljava/lang/Object;
107+
protected abstract fun getInitial ()Ljava/lang/Object;
108+
public final fun getUpdates ()Lkotlinx/coroutines/flow/Flow;
109+
public fun getValue ()Ljava/lang/Object;
110+
protected final fun update (Ljava/lang/Object;)V
111+
}
112+
103113
public abstract class com/eygraber/vice/sources/StateFlowSource : com/eygraber/vice/ViceSource {
104114
public static final field $stable I
105115
public fun <init> ()V

vice-sources/api/vice-sources.klib.api

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,20 @@ abstract class <#A: kotlin/Any> com.eygraber.vice.sources/SaveableMutableStateSo
5555
open fun currentState(androidx.compose.runtime/Composer?, kotlin/Int): #A // com.eygraber.vice.sources/SaveableMutableStateSource.currentState|currentState(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
5656
}
5757

58+
abstract class <#A: kotlin/Any> com.eygraber.vice.sources/SerializableMutableStateSource : com.eygraber.vice.sources/StateSource<#A> { // com.eygraber.vice.sources/SerializableMutableStateSource|null[0]
59+
constructor <init>(kotlinx.serialization/KSerializer<#A>) // com.eygraber.vice.sources/SerializableMutableStateSource.<init>|<init>(kotlinx.serialization.KSerializer<1:0>){}[0]
60+
61+
abstract val initial // com.eygraber.vice.sources/SerializableMutableStateSource.initial|{}initial[0]
62+
abstract fun <get-initial>(): #A // com.eygraber.vice.sources/SerializableMutableStateSource.initial.<get-initial>|<get-initial>(){}[0]
63+
final val updates // com.eygraber.vice.sources/SerializableMutableStateSource.updates|{}updates[0]
64+
final fun <get-updates>(): kotlinx.coroutines.flow/Flow<#A> // com.eygraber.vice.sources/SerializableMutableStateSource.updates.<get-updates>|<get-updates>(){}[0]
65+
open val value // com.eygraber.vice.sources/SerializableMutableStateSource.value|{}value[0]
66+
open fun <get-value>(): #A // com.eygraber.vice.sources/SerializableMutableStateSource.value.<get-value>|<get-value>(){}[0]
67+
68+
final fun update(#A) // com.eygraber.vice.sources/SerializableMutableStateSource.update|update(1:0){}[0]
69+
open fun currentState(androidx.compose.runtime/Composer?, kotlin/Int): #A // com.eygraber.vice.sources/SerializableMutableStateSource.currentState|currentState(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
70+
}
71+
5872
abstract class <#A: kotlin/Any?> com.eygraber.vice.sources/DerivedStateSource : com.eygraber.vice.sources/StateSource<#A> { // com.eygraber.vice.sources/DerivedStateSource|null[0]
5973
constructor <init>() // com.eygraber.vice.sources/DerivedStateSource.<init>|<init>(){}[0]
6074

@@ -157,6 +171,7 @@ final val com.eygraber.vice.sources/com_eygraber_vice_sources_LoadableFlowSource
157171
final val com.eygraber.vice.sources/com_eygraber_vice_sources_LoadableSource$stableprop // com.eygraber.vice.sources/com_eygraber_vice_sources_LoadableSource$stableprop|#static{}com_eygraber_vice_sources_LoadableSource$stableprop[0]
158172
final val com.eygraber.vice.sources/com_eygraber_vice_sources_MutableStateSource$stableprop // com.eygraber.vice.sources/com_eygraber_vice_sources_MutableStateSource$stableprop|#static{}com_eygraber_vice_sources_MutableStateSource$stableprop[0]
159173
final val com.eygraber.vice.sources/com_eygraber_vice_sources_SaveableMutableStateSource$stableprop // com.eygraber.vice.sources/com_eygraber_vice_sources_SaveableMutableStateSource$stableprop|#static{}com_eygraber_vice_sources_SaveableMutableStateSource$stableprop[0]
174+
final val com.eygraber.vice.sources/com_eygraber_vice_sources_SerializableMutableStateSource$stableprop // com.eygraber.vice.sources/com_eygraber_vice_sources_SerializableMutableStateSource$stableprop|#static{}com_eygraber_vice_sources_SerializableMutableStateSource$stableprop[0]
160175
final val com.eygraber.vice.sources/com_eygraber_vice_sources_StateFlowSource$stableprop // com.eygraber.vice.sources/com_eygraber_vice_sources_StateFlowSource$stableprop|#static{}com_eygraber_vice_sources_StateFlowSource$stableprop[0]
161176

162177
final fun com.eygraber.vice.loadable/com_eygraber_vice_loadable_ViceLoadable_Loaded$stableprop_getter(): kotlin/Int // com.eygraber.vice.loadable/com_eygraber_vice_loadable_ViceLoadable_Loaded$stableprop_getter|com_eygraber_vice_loadable_ViceLoadable_Loaded$stableprop_getter(){}[0]
@@ -167,6 +182,7 @@ final fun com.eygraber.vice.sources/com_eygraber_vice_sources_LoadableFlowSource
167182
final fun com.eygraber.vice.sources/com_eygraber_vice_sources_LoadableSource$stableprop_getter(): kotlin/Int // com.eygraber.vice.sources/com_eygraber_vice_sources_LoadableSource$stableprop_getter|com_eygraber_vice_sources_LoadableSource$stableprop_getter(){}[0]
168183
final fun com.eygraber.vice.sources/com_eygraber_vice_sources_MutableStateSource$stableprop_getter(): kotlin/Int // com.eygraber.vice.sources/com_eygraber_vice_sources_MutableStateSource$stableprop_getter|com_eygraber_vice_sources_MutableStateSource$stableprop_getter(){}[0]
169184
final fun com.eygraber.vice.sources/com_eygraber_vice_sources_SaveableMutableStateSource$stableprop_getter(): kotlin/Int // com.eygraber.vice.sources/com_eygraber_vice_sources_SaveableMutableStateSource$stableprop_getter|com_eygraber_vice_sources_SaveableMutableStateSource$stableprop_getter(){}[0]
185+
final fun com.eygraber.vice.sources/com_eygraber_vice_sources_SerializableMutableStateSource$stableprop_getter(): kotlin/Int // com.eygraber.vice.sources/com_eygraber_vice_sources_SerializableMutableStateSource$stableprop_getter|com_eygraber_vice_sources_SerializableMutableStateSource$stableprop_getter(){}[0]
170186
final fun com.eygraber.vice.sources/com_eygraber_vice_sources_StateFlowSource$stableprop_getter(): kotlin/Int // com.eygraber.vice.sources/com_eygraber_vice_sources_StateFlowSource$stableprop_getter|com_eygraber_vice_sources_StateFlowSource$stableprop_getter(){}[0]
171187
final inline fun <#A: kotlin/Any?, #B: kotlin/Any?> (com.eygraber.vice.loadable/ViceLoadable<#A>).com.eygraber.vice.loadable/map(kotlin/Function1<#A, com.eygraber.vice.loadable/ViceLoadable<#B>>): com.eygraber.vice.loadable/ViceLoadable<#B> // com.eygraber.vice.loadable/map|[email protected]<0:0>(kotlin.Function1<0:0,com.eygraber.vice.loadable.ViceLoadable<0:1>>){0§<kotlin.Any?>;1§<kotlin.Any?>}[0]
172188
final inline fun <#A: kotlin/Any?, #B: kotlin/Any?> (com.eygraber.vice.loadable/ViceLoadable<#A>).com.eygraber.vice.loadable/mapValue(kotlin/Function1<#A, #B>): com.eygraber.vice.loadable/ViceLoadable<#B> // com.eygraber.vice.loadable/mapValue|[email protected]<0:0>(kotlin.Function1<0:0,0:1>){0§<kotlin.Any?>;1§<kotlin.Any?>}[0]

vice-sources/build.gradle.kts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import org.jetbrains.compose.ExperimentalComposeLibrary
12
import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation
23

34
plugins {
@@ -32,8 +33,18 @@ kotlin {
3233

3334
commonTest {
3435
dependencies {
35-
implementation(kotlin("test-common"))
36-
implementation(kotlin("test-annotations-common"))
36+
implementation(kotlin("test"))
37+
}
38+
}
39+
40+
jvmTest {
41+
dependencies {
42+
implementation(compose.desktop.currentOs)
43+
44+
implementation(compose.foundation)
45+
46+
@OptIn(ExperimentalComposeLibrary::class)
47+
implementation(compose.uiTest)
3748
}
3849
}
3950
}
Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,36 @@
11
package com.eygraber.vice.sources
22

33
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.getValue
45
import androidx.compose.runtime.mutableStateOf
56
import androidx.compose.runtime.saveable.Saver
67
import androidx.compose.runtime.saveable.rememberSaveable
8+
import androidx.compose.runtime.setValue
79
import androidx.compose.runtime.snapshotFlow
810
import kotlinx.coroutines.flow.Flow
911

1012
public abstract class SaveableMutableStateSource<T : Any>(
1113
private val saver: Saver<T, out Any>? = null,
1214
) : StateSource<T> {
13-
private val state by lazy {
14-
mutableStateOf(initial)
15-
}
15+
private var stateOfState by mutableStateOf(mutableStateOf(initial))
1616

17-
public val updates: Flow<T> get() = snapshotFlow { state.value }
17+
public val updates: Flow<T> get() = snapshotFlow { stateOfState.value }
1818

19-
override val value: T get() = state.value
19+
override val value: T get() = stateOfState.value
2020

2121
protected abstract val initial: T
2222

2323
protected fun update(value: T) {
24-
state.value = value
24+
stateOfState.value = value
2525
}
2626

2727
@Composable
28-
override fun currentState(): T = when(saver) {
29-
null -> rememberSaveable { state }.value
30-
else -> rememberSaveable(stateSaver = saver) { state }.value
28+
override fun currentState(): T {
29+
val rememberedState = when(saver) {
30+
null -> rememberSaveable { mutableStateOf(initial) }
31+
else -> rememberSaveable(stateSaver = saver) { mutableStateOf(initial) }
32+
}
33+
stateOfState = rememberedState
34+
return stateOfState.value
3135
}
3236
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.eygraber.vice.sources
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.getValue
5+
import androidx.compose.runtime.mutableStateOf
6+
import androidx.compose.runtime.saveable.rememberSerializable
7+
import androidx.compose.runtime.setValue
8+
import androidx.compose.runtime.snapshotFlow
9+
import kotlinx.coroutines.flow.Flow
10+
import kotlinx.serialization.KSerializer
11+
12+
public abstract class SerializableMutableStateSource<T : Any>(
13+
private val stateSerializer: KSerializer<T>,
14+
) : StateSource<T> {
15+
private var stateOfState by mutableStateOf(mutableStateOf(initial))
16+
17+
public val updates: Flow<T> get() = snapshotFlow { stateOfState.value }
18+
19+
override val value: T get() = stateOfState.value
20+
21+
protected abstract val initial: T
22+
23+
protected fun update(value: T) {
24+
stateOfState.value = value
25+
}
26+
27+
@Composable
28+
override fun currentState(): T {
29+
val rememberedState = rememberSerializable(stateSerializer = stateSerializer) {
30+
mutableStateOf(initial)
31+
}
32+
stateOfState = rememberedState
33+
return stateOfState.value
34+
}
35+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package com.eygraber.vice.sources
2+
3+
import androidx.compose.runtime.DisposableEffect
4+
import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
5+
import androidx.compose.runtime.saveable.SaveableStateRegistry
6+
import androidx.compose.runtime.saveable.Saver
7+
import androidx.compose.ui.test.ExperimentalTestApi
8+
import androidx.compose.ui.test.runComposeUiTest
9+
import kotlin.test.Test
10+
import kotlin.test.assertContentEquals
11+
12+
@OptIn(ExperimentalTestApi::class)
13+
class SaveableMutableStateSourceTest {
14+
@Test
15+
fun `test that simple saveable mutable state source works`() = runComposeUiTest {
16+
val simpleSource = SimpleSource()
17+
val values = mutableSetOf<Int>()
18+
val disposedValues = mutableListOf<Boolean>()
19+
20+
setContent {
21+
values += simpleSource.currentState()
22+
23+
DisposableEffect(Unit) {
24+
disposedValues += false
25+
26+
onDispose {
27+
disposedValues += true
28+
}
29+
}
30+
}
31+
32+
assertContentEquals(listOf(1), values)
33+
assertContentEquals(listOf(false), disposedValues)
34+
simpleSource.increment()
35+
awaitIdle()
36+
assertContentEquals(listOf(1, 2), values)
37+
assertContentEquals(listOf(false), disposedValues)
38+
}
39+
40+
@Test
41+
fun `test that simple saveable mutable state source works across recreation`() = runComposeUiTest {
42+
val simpleSource = SimpleSource()
43+
val values = mutableSetOf<Int>()
44+
val disposedValues = mutableListOf<Boolean>()
45+
val restorationTester = ViceStateRestorationTester(this)
46+
47+
LocalSaveableStateRegistry.providesDefault(
48+
SaveableStateRegistry(
49+
restoredValues = null,
50+
canBeSaved = { true },
51+
),
52+
)
53+
54+
restorationTester.setContent {
55+
values += simpleSource.currentState()
56+
57+
DisposableEffect(Unit) {
58+
disposedValues += false
59+
60+
onDispose {
61+
disposedValues += true
62+
}
63+
}
64+
}
65+
66+
assertContentEquals(listOf(1), values)
67+
assertContentEquals(listOf(false), disposedValues)
68+
simpleSource.increment()
69+
awaitIdle()
70+
assertContentEquals(listOf(1, 2), values)
71+
assertContentEquals(listOf(false), disposedValues)
72+
73+
restorationTester.emulateSaveAndRestore()
74+
75+
assertContentEquals(listOf(1, 2), values)
76+
assertContentEquals(listOf(false, true, false), disposedValues)
77+
simpleSource.increment()
78+
awaitIdle()
79+
assertContentEquals(listOf(1, 2, 3), values)
80+
assertContentEquals(listOf(false, true, false), disposedValues)
81+
}
82+
83+
@Test
84+
fun `test that complex saveable mutable state source works`() = runComposeUiTest {
85+
val complexSource = ComplexSource()
86+
val values = mutableSetOf<ComplexData>()
87+
val disposedValues = mutableListOf<Boolean>()
88+
89+
setContent {
90+
values += complexSource.currentState()
91+
92+
DisposableEffect(Unit) {
93+
disposedValues += false
94+
95+
onDispose {
96+
disposedValues += true
97+
}
98+
}
99+
}
100+
101+
assertContentEquals(listOf(ComplexData(1)), values)
102+
assertContentEquals(listOf(false), disposedValues)
103+
complexSource.increment()
104+
awaitIdle()
105+
assertContentEquals(listOf(ComplexData(1), ComplexData(2)), values)
106+
assertContentEquals(listOf(false), disposedValues)
107+
}
108+
109+
@Test
110+
fun `test that complex saveable mutable state source works across recreation`() = runComposeUiTest {
111+
val complexSource = ComplexSource()
112+
val values = mutableSetOf<ComplexData>()
113+
val disposedValues = mutableListOf<Boolean>()
114+
val restorationTester = ViceStateRestorationTester(this)
115+
116+
LocalSaveableStateRegistry.providesDefault(
117+
SaveableStateRegistry(
118+
restoredValues = null,
119+
canBeSaved = { true },
120+
),
121+
)
122+
123+
restorationTester.setContent {
124+
values += complexSource.currentState()
125+
126+
DisposableEffect(Unit) {
127+
disposedValues += false
128+
129+
onDispose {
130+
disposedValues += true
131+
}
132+
}
133+
}
134+
135+
assertContentEquals(listOf(ComplexData(1)), values)
136+
assertContentEquals(listOf(false), disposedValues)
137+
complexSource.increment()
138+
awaitIdle()
139+
assertContentEquals(listOf(ComplexData(1), ComplexData(2)), values)
140+
assertContentEquals(listOf(false), disposedValues)
141+
142+
restorationTester.emulateSaveAndRestore()
143+
144+
assertContentEquals(listOf(ComplexData(1), ComplexData(2)), values)
145+
assertContentEquals(listOf(false, true, false), disposedValues)
146+
complexSource.increment()
147+
awaitIdle()
148+
assertContentEquals(listOf(ComplexData(1), ComplexData(2), ComplexData(3)), values)
149+
assertContentEquals(listOf(false, true, false), disposedValues)
150+
}
151+
152+
private class SimpleSource : SaveableMutableStateSource<Int>() {
153+
override val initial: Int = 1
154+
155+
fun increment() {
156+
update(value + 1)
157+
}
158+
}
159+
160+
private data class ComplexData(val data: Int)
161+
162+
private class ComplexSource : SaveableMutableStateSource<ComplexData>(
163+
saver = Saver(
164+
save = {
165+
it.data
166+
},
167+
restore = {
168+
ComplexData(it)
169+
},
170+
),
171+
) {
172+
override val initial: ComplexData = ComplexData(1)
173+
174+
fun increment() {
175+
update(ComplexData(value.data + 1))
176+
}
177+
}
178+
}

0 commit comments

Comments
 (0)