Skip to content

Commit c22d93b

Browse files
WIP adding real saveable support
Depends on workflow-runtime having an android source set and some other refactors.
1 parent f01306e commit c22d93b

File tree

2 files changed

+182
-0
lines changed

2 files changed

+182
-0
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package com.squareup.workflow1.android
2+
3+
import android.os.Binder
4+
import android.os.Bundle
5+
import android.os.Parcelable
6+
import android.util.Size
7+
import android.util.SizeF
8+
import android.util.SparseArray
9+
import androidx.compose.runtime.neverEqualPolicy
10+
import androidx.compose.runtime.referentialEqualityPolicy
11+
import androidx.compose.runtime.saveable.SaveableStateRegistry
12+
import androidx.compose.runtime.snapshots.SnapshotMutableState
13+
import androidx.compose.runtime.structuralEqualityPolicy
14+
import com.squareup.workflow1.Snapshot
15+
import java.io.Serializable
16+
17+
/**
18+
* A [SaveableStateRegistry] that can save and restore anything that can be saved in a [Bundle].
19+
*
20+
* Similar to Compose Android's `DisposableSaveableStateRegistry`.
21+
*/
22+
internal class BundleSaveableStateRegistry private constructor(
23+
saveableStateRegistry: SaveableStateRegistry
24+
) : SaveableStateRegistry by saveableStateRegistry {
25+
constructor(restoredValues: Map<String, List<Any?>>?) : this(
26+
SaveableStateRegistry(restoredValues, ::canBeSavedToBundle)
27+
)
28+
29+
// TODO move the functions from SnapshotParcels.kt into runtime-android.
30+
// constructor(snapshot: Snapshot) : this(snapshot.toParcelable<Bundle>().toMap())
31+
//
32+
// fun toSnapshot(): Snapshot = performSave().toBundle().toSnapshot()
33+
}
34+
35+
/**
36+
* Checks that [value] can be stored inside [Bundle].
37+
*/
38+
private fun canBeSavedToBundle(value: Any): Boolean {
39+
// SnapshotMutableStateImpl is Parcelable, but we do extra checks
40+
if (value is SnapshotMutableState<*>) {
41+
if (value.policy === neverEqualPolicy<Any?>() ||
42+
value.policy === structuralEqualityPolicy<Any?>() ||
43+
value.policy === referentialEqualityPolicy<Any?>()
44+
) {
45+
val stateValue = value.value
46+
return if (stateValue == null) true else canBeSavedToBundle(stateValue)
47+
} else {
48+
return false
49+
}
50+
}
51+
// lambdas in Kotlin implement Serializable, but will crash if you really try to save them.
52+
// we check for both Function and Serializable (see kotlin.jvm.internal.Lambda) to support
53+
// custom user defined classes implementing Function interface.
54+
if (value is Function<*> && value is Serializable) {
55+
return false
56+
}
57+
for (cl in AcceptableClasses) {
58+
if (cl.isInstance(value)) {
59+
return true
60+
}
61+
}
62+
return false
63+
}
64+
65+
/**
66+
* Contains Classes which can be stored inside [Bundle].
67+
*
68+
* Some of the classes are not added separately because:
69+
*
70+
* - These classes implement Serializable:
71+
* - Arrays (DoubleArray, BooleanArray, IntArray, LongArray, ByteArray, FloatArray, ShortArray,
72+
* CharArray, Array<Parcelable>, Array<String>)
73+
* - ArrayList
74+
* - Primitives (Boolean, Int, Long, Double, Float, Byte, Short, Char) will be boxed when casted
75+
* to Any, and all the boxed classes implements Serializable.
76+
* - This class implements Parcelable:
77+
* - Bundle
78+
*
79+
* Note: it is simplified copy of the array from SavedStateHandle (lifecycle-viewmodel-savedstate).
80+
*/
81+
private val AcceptableClasses = arrayOf(
82+
Serializable::class.java,
83+
Parcelable::class.java,
84+
String::class.java,
85+
SparseArray::class.java,
86+
Binder::class.java,
87+
Size::class.java,
88+
SizeF::class.java
89+
)
90+
91+
@Suppress("DEPRECATION")
92+
private fun Bundle.toMap(): Map<String, List<Any?>>? {
93+
val map = mutableMapOf<String, List<Any?>>()
94+
this.keySet().forEach { key ->
95+
@Suppress("UNCHECKED_CAST")
96+
val list = getParcelableArrayList<Parcelable?>(key) as ArrayList<Any?>
97+
map[key] = list
98+
}
99+
return map
100+
}
101+
102+
private fun Map<String, List<Any?>>.toBundle(): Bundle {
103+
val bundle = Bundle()
104+
forEach { (key, list) ->
105+
val arrayList = if (list is ArrayList<Any?>) list else ArrayList(list)
106+
@Suppress("UNCHECKED_CAST")
107+
bundle.putParcelableArrayList(key, arrayList as ArrayList<Parcelable?>)
108+
}
109+
return bundle
110+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.squareup.workflow1.internal.compose.runtime
2+
3+
import androidx.compose.runtime.saveable.SaveableStateRegistry
4+
import com.squareup.workflow1.Snapshot
5+
import com.squareup.workflow1.parse
6+
import com.squareup.workflow1.readUtf8WithLength
7+
import com.squareup.workflow1.writeUtf8WithLength
8+
import okio.BufferedSink
9+
import okio.ByteString
10+
import java.io.ObjectInputStream
11+
import java.io.ObjectOutputStream
12+
import java.io.Serializable
13+
14+
/**
15+
* A [SaveableStateRegistry] that can save and restore anything that is [Serializable].
16+
*/
17+
internal class SerializableSaveableStateRegistry private constructor(
18+
saveableStateRegistry: SaveableStateRegistry
19+
) : SaveableStateRegistry by saveableStateRegistry {
20+
constructor(restoredValues: Map<String, List<Any?>>?) : this(
21+
SaveableStateRegistry(restoredValues, ::canBeSavedAsSerializable)
22+
)
23+
24+
constructor(snapshot: Snapshot) : this(snapshot.bytes.toMap())
25+
26+
fun toSnapshot(): Snapshot = Snapshot.write { sink ->
27+
performSave().writeTo(sink)
28+
}
29+
}
30+
31+
/**
32+
* Checks that [value] can be stored as a [Serializable].
33+
*/
34+
private fun canBeSavedAsSerializable(value: Any): Boolean {
35+
if (value !is Serializable) return false
36+
37+
// lambdas in Kotlin implement Serializable, but will crash if you really try to save them.
38+
// we check for both Function and Serializable (see kotlin.jvm.internal.Lambda) to support
39+
// custom user defined classes implementing Function interface.
40+
if (value is Function<*>) return false
41+
42+
return true
43+
}
44+
45+
private fun ByteString.toMap(): Map<String, List<Any?>>? {
46+
return parse { source ->
47+
val size = source.readInt()
48+
if (size == 0) return null
49+
50+
val inputStream = ObjectInputStream(source.inputStream())
51+
buildMap(capacity = size) {
52+
repeat(size) {
53+
val key = source.readUtf8WithLength()
54+
55+
@Suppress("UNCHECKED_CAST")
56+
val arrayList = inputStream.readObject() as ArrayList<Any?>
57+
put(key, arrayList)
58+
}
59+
}
60+
}
61+
}
62+
63+
private fun Map<String, List<Any?>>.writeTo(sink: BufferedSink) {
64+
sink.writeInt(values.size)
65+
val outputStream = ObjectOutputStream(sink.outputStream())
66+
values.forEach { (key, list) ->
67+
val arrayList = if (list is ArrayList<Any?>) list else ArrayList(list)
68+
sink.writeUtf8WithLength(key)
69+
outputStream.writeObject(arrayList)
70+
outputStream.flush()
71+
}
72+
}

0 commit comments

Comments
 (0)