Merge changes from topic "fix-builtin-serializers" into androidx-main
* changes:
Make JavaSerializableSerializer and ParcelableSerializer abstract
Add platform-specific encoding and decoding to SavedState codec tests
Remove generic from CharSequenceSerializer
diff --git a/savedstate/savedstate-samples/src/main/java/androidx/savedstate/SavedStateCodecSamples.kt b/savedstate/savedstate-samples/src/main/java/androidx/savedstate/SavedStateCodecSamples.kt
index 8fd4b86..616baa6 100644
--- a/savedstate/savedstate-samples/src/main/java/androidx/savedstate/SavedStateCodecSamples.kt
+++ b/savedstate/savedstate-samples/src/main/java/androidx/savedstate/SavedStateCodecSamples.kt
@@ -18,6 +18,8 @@
package androidx.savedstate
+import android.os.Parcel
+import android.os.Parcelable
import androidx.annotation.Sampled
import androidx.savedstate.serialization.decodeFromSavedState
import androidx.savedstate.serialization.encodeToSavedState
@@ -96,7 +98,6 @@
val uuid = decodeFromSavedState(UUIDSerializer(), uuidSavedState)
}
-@Suppress("SERIALIZER_TYPE_INCOMPATIBLE") // The lint warning does not show up for external users.
@Sampled
fun savedStateSerializer() {
@Serializable
@@ -125,20 +126,36 @@
)
}
+private class MyJavaSerializable : java.io.Serializable
+
+private class MyJavaSerializableSerializer : JavaSerializableSerializer<MyJavaSerializable>()
+
@Sampled
fun serializableSerializer() {
@Serializable
data class MyModel(
- @Serializable(with = JavaSerializableSerializer::class)
- val serializable: java.io.Serializable
+ @Serializable(with = MyJavaSerializableSerializer::class)
+ val serializable: MyJavaSerializable
)
}
+private class MyParcelable : Parcelable {
+ override fun describeContents(): Int {
+ TODO("Not yet implemented")
+ }
+
+ override fun writeToParcel(dest: Parcel, flags: Int) {
+ TODO("Not yet implemented")
+ }
+}
+
+private class MyParcelableSerializer : ParcelableSerializer<MyParcelable>()
+
@Sampled
fun parcelableSerializer() {
@Serializable
data class MyModel(
- @Serializable(with = ParcelableSerializer::class) val parcelable: android.os.Parcelable
+ @Serializable(with = MyParcelableSerializer::class) val parcelable: MyParcelable
)
}
@@ -172,13 +189,11 @@
fun charSequenceListSerializer() {
@Serializable
class MyModel(
- @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(with = CharSequenceListSerializer::class)
val charSequenceList: List<CharSequence>
)
}
-@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Sampled
fun parcelableListSerializer() {
@Serializable
diff --git a/savedstate/savedstate/api/current.txt b/savedstate/savedstate/api/current.txt
index a3e7083..8a63bfe 100644
--- a/savedstate/savedstate/api/current.txt
+++ b/savedstate/savedstate/api/current.txt
@@ -195,12 +195,12 @@
property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
}
- public class CharSequenceSerializer<T extends java.lang.CharSequence> implements kotlinx.serialization.KSerializer<T> {
+ public final class CharSequenceSerializer implements kotlinx.serialization.KSerializer<java.lang.CharSequence> {
ctor public CharSequenceSerializer();
- method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
- method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
- method public final void serialize(kotlinx.serialization.encoding.Encoder encoder, T value);
- property public final kotlinx.serialization.descriptors.SerialDescriptor descriptor;
+ method public CharSequence deserialize(kotlinx.serialization.encoding.Decoder decoder);
+ method public kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
+ method public void serialize(kotlinx.serialization.encoding.Encoder encoder, CharSequence value);
+ property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
}
public final class IBinderSerializer implements kotlinx.serialization.KSerializer<android.os.IBinder> {
@@ -211,7 +211,7 @@
property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
}
- public class JavaSerializableSerializer<T extends java.io.Serializable> implements kotlinx.serialization.KSerializer<T> {
+ public abstract class JavaSerializableSerializer<T extends java.io.Serializable> implements kotlinx.serialization.KSerializer<T> {
ctor public JavaSerializableSerializer();
method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
@@ -235,7 +235,7 @@
property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
}
- public class ParcelableSerializer<T extends android.os.Parcelable> implements kotlinx.serialization.KSerializer<T> {
+ public abstract class ParcelableSerializer<T extends android.os.Parcelable> implements kotlinx.serialization.KSerializer<T> {
ctor public ParcelableSerializer();
method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
diff --git a/savedstate/savedstate/api/restricted_current.txt b/savedstate/savedstate/api/restricted_current.txt
index eb7a636..0f65840 100644
--- a/savedstate/savedstate/api/restricted_current.txt
+++ b/savedstate/savedstate/api/restricted_current.txt
@@ -220,12 +220,12 @@
property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
}
- public class CharSequenceSerializer<T extends java.lang.CharSequence> implements kotlinx.serialization.KSerializer<T> {
+ public final class CharSequenceSerializer implements kotlinx.serialization.KSerializer<java.lang.CharSequence> {
ctor public CharSequenceSerializer();
- method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
- method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
- method public final void serialize(kotlinx.serialization.encoding.Encoder encoder, T value);
- property public final kotlinx.serialization.descriptors.SerialDescriptor descriptor;
+ method public CharSequence deserialize(kotlinx.serialization.encoding.Decoder decoder);
+ method public kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
+ method public void serialize(kotlinx.serialization.encoding.Encoder encoder, CharSequence value);
+ property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
}
public final class IBinderSerializer implements kotlinx.serialization.KSerializer<android.os.IBinder> {
@@ -236,7 +236,7 @@
property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
}
- public class JavaSerializableSerializer<T extends java.io.Serializable> implements kotlinx.serialization.KSerializer<T> {
+ public abstract class JavaSerializableSerializer<T extends java.io.Serializable> implements kotlinx.serialization.KSerializer<T> {
ctor public JavaSerializableSerializer();
method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
@@ -260,7 +260,7 @@
property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
}
- public class ParcelableSerializer<T extends android.os.Parcelable> implements kotlinx.serialization.KSerializer<T> {
+ public abstract class ParcelableSerializer<T extends android.os.Parcelable> implements kotlinx.serialization.KSerializer<T> {
ctor public ParcelableSerializer();
method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
diff --git a/savedstate/savedstate/src/androidInstrumentedTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt b/savedstate/savedstate/src/androidInstrumentedTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt
new file mode 100644
index 0000000..edf79cc
--- /dev/null
+++ b/savedstate/savedstate/src/androidInstrumentedTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.savedstate
+
+import android.os.Parcel
+
+actual fun platformEncodeDecode(savedState: SavedState): SavedState {
+ val parcel =
+ Parcel.obtain().apply {
+ savedState.writeToParcel(this, 0)
+ setDataPosition(0)
+ }
+ return SavedState.CREATOR.createFromParcel(parcel)
+}
diff --git a/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/serialization/serializers/BuiltInSerializer.android.kt b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/serialization/serializers/BuiltInSerializer.android.kt
index f0dacc5..071f4c1 100644
--- a/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/serialization/serializers/BuiltInSerializer.android.kt
+++ b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/serialization/serializers/BuiltInSerializer.android.kt
@@ -107,10 +107,10 @@
* @see androidx.savedstate.serialization.decodeFromSavedState
*/
@OptIn(ExperimentalSerializationApi::class)
-public open class CharSequenceSerializer<T : CharSequence> : KSerializer<T> {
- final override val descriptor: SerialDescriptor = buildClassSerialDescriptor("CharSequence")
+public class CharSequenceSerializer : KSerializer<CharSequence> {
+ override val descriptor: SerialDescriptor = buildClassSerialDescriptor("CharSequence")
- final override fun serialize(encoder: Encoder, value: T) {
+ override fun serialize(encoder: Encoder, value: CharSequence) {
require(encoder is SavedStateEncoder) {
encoderErrorMessage(descriptor.serialName, encoder)
}
@@ -118,17 +118,18 @@
}
@Suppress("UNCHECKED_CAST")
- final override fun deserialize(decoder: Decoder): T {
+ override fun deserialize(decoder: Decoder): CharSequence {
require(decoder is SavedStateDecoder) {
decoderErrorMessage(descriptor.serialName, decoder)
}
- return decoder.run { savedState.read { getCharSequence(key) as T } }
+ return decoder.run { savedState.read { getCharSequence(key) } }
}
}
/**
* A serializer for [java.io.Serializable]. This serializer uses [SavedState]'s API directly to
- * save/load a [java.io.Serializable].
+ * save/load a [java.io.Serializable]. You must extend this serializer for each of your
+ * [java.io.Serializable] subclasses.
*
* Note that this serializer should be used with [SavedStateEncoder] or [SavedStateDecoder] only.
* Using it with other Encoders/Decoders may throw [IllegalArgumentException].
@@ -138,7 +139,7 @@
* @see androidx.savedstate.serialization.decodeFromSavedState
*/
@OptIn(ExperimentalSerializationApi::class)
-public open class JavaSerializableSerializer<T : JavaSerializable> : KSerializer<T> {
+public abstract class JavaSerializableSerializer<T : JavaSerializable> : KSerializer<T> {
final override val descriptor: SerialDescriptor = buildClassSerialDescriptor("JavaSerializable")
final override fun serialize(encoder: Encoder, value: T) {
@@ -159,7 +160,7 @@
/**
* A serializer for [Parcelable]. This serializer uses [SavedState]'s API directly to save/load a
- * [Parcelable].
+ * [Parcelable]. You must extend this serializer for each of your [Parcelable] subclasses.
*
* Note that this serializer should be used with [SavedStateEncoder] or [SavedStateDecoder] only.
* Using it with other Encoders/Decoders may throw [IllegalArgumentException].
@@ -169,7 +170,7 @@
* @see androidx.savedstate.serialization.decodeFromSavedState
*/
@OptIn(ExperimentalSerializationApi::class)
-public open class ParcelableSerializer<T : Parcelable> : KSerializer<T> {
+public abstract class ParcelableSerializer<T : Parcelable> : KSerializer<T> {
final override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Parcelable")
final override fun serialize(encoder: Encoder, value: T) {
diff --git a/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecAndroidTest.android.kt b/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecAndroidTest.android.kt
index 2c34ddb..743c3e4 100644
--- a/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecAndroidTest.android.kt
+++ b/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecAndroidTest.android.kt
@@ -26,7 +26,6 @@
import android.util.SizeF
import android.util.SparseArray
import androidx.core.os.bundleOf
-import androidx.core.util.forEach
import androidx.kruth.assertThat
import androidx.kruth.assertThrows
import androidx.savedstate.SavedStateCodecTestUtils.encodeDecode
@@ -59,7 +58,6 @@
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.encoding.encodeStructure
-import kotlinx.serialization.serializer
@ExperimentalSerializationApi
internal class SavedStateCodecAndroidTest : RobolectricTest() {
@@ -100,18 +98,7 @@
"SERIALIZER_TYPE_INCOMPATIBLE"
) // The lint warning does not show up for external users.
@Serializable
- class MyClass(@Serializable(with = SavedStateSerializer::class) val s: Bundle) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
- other as MyClass
- return s.read { contentDeepEquals(other.s) }
- }
-
- override fun hashCode(): Int {
- return s.read { contentDeepHashCode() }
- }
- }
+ class MyClass(@Serializable(with = SavedStateSerializer::class) val s: Bundle)
MyClass(
bundleOf(
"i" to 1,
@@ -120,19 +107,24 @@
"ss" to bundleOf("s" to "bar")
)
)
- .encodeDecode {
- assertThat(size()).isEqualTo(1)
- getSavedState("s").read {
- assertThat(size()).isEqualTo(4)
- assertThat(getInt("i")).isEqualTo(1)
- assertThat(getString("s")).isEqualTo("foo")
- assertThat(getIntArray("a")).isEqualTo(intArrayOf(1, 3, 5))
- getSavedState("ss").read {
- assertThat(size()).isEqualTo(1)
- assertThat(getString("s")).isEqualTo("bar")
+ .encodeDecode(
+ checkDecoded = { decoded, original ->
+ assertThat(decoded.s.read { contentDeepEquals(original.s) }).isTrue()
+ },
+ checkEncoded = {
+ assertThat(size()).isEqualTo(1)
+ getSavedState("s").read {
+ assertThat(size()).isEqualTo(4)
+ assertThat(getInt("i")).isEqualTo(1)
+ assertThat(getString("s")).isEqualTo("foo")
+ assertThat(getIntArray("a")).isEqualTo(intArrayOf(1, 3, 5))
+ getSavedState("ss").read {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getString("s")).isEqualTo("bar")
+ }
}
}
- }
+ )
// Bundle at root.
val origin = bundleOf("i" to 3, "s" to "foo", "d" to 3.14)
@@ -180,7 +172,8 @@
@Serializable
data class SerializableContainer(
- @Serializable(with = JavaSerializableSerializer::class) val value: java.io.Serializable
+ @Serializable(with = CustomJavaSerializableSerializer::class)
+ val value: java.io.Serializable
)
val myJavaSerializable = MyJavaSerializable(3, "foo", 3.14)
SerializableContainer(myJavaSerializable).encodeDecode {
@@ -191,7 +184,7 @@
@Serializable
data class ParcelableContainer(
- @Serializable(with = ParcelableSerializer::class) val value: Parcelable
+ @Serializable(with = CustomParcelableSerializer::class) val value: Parcelable
)
val myParcelable = MyParcelable(3, "foo", 3.14)
ParcelableContainer(myParcelable).encodeDecode {
@@ -257,19 +250,9 @@
error("VERSION.SDK_INT < Q")
}
+ @Suppress("ArrayInDataClass")
@Serializable
- data class CharSequenceArrayContainer(val value: Array<out CharSequence>) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
- other as CharSequenceArrayContainer
- return value.contentEquals(other.value)
- }
-
- override fun hashCode(): Int {
- return value.contentHashCode()
- }
- }
+ data class CharSequenceArrayContainer(val value: Array<out CharSequence>)
assertThrows<SerializationException> {
CharSequenceArrayContainer(arrayOf("foo", "bar")).encodeDecode {}
}
@@ -281,20 +264,10 @@
@Test
fun concreteTypesInsteadOfInterfaceTypes() {
- @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
- @Serializable
- data class CharSequenceContainer(
- @Serializable(with = CharSequenceSerializer::class) val value: String
- )
- CharSequenceContainer("foo").encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getCharSequence("value")).isEqualTo("foo")
- }
-
- @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable
data class SerializableContainer(
- @Serializable(with = JavaSerializableSerializer::class) val value: MyJavaSerializable
+ @Serializable(with = MyJavaSerializableAsJavaSerializableSerializer::class)
+ val value: MyJavaSerializable
)
val myJavaSerializable = MyJavaSerializable(3, "foo", 3.14)
SerializableContainer(myJavaSerializable).encodeDecode {
@@ -303,10 +276,9 @@
.isEqualTo(myJavaSerializable)
}
- @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable
data class ParcelableContainer(
- @Serializable(with = ParcelableSerializer::class) val value: MyParcelable
+ @Serializable(with = MyParcelableAsParcelableSerializer::class) val value: MyParcelable
)
val myParcelable = MyParcelable(3, "foo", 3.14)
ParcelableContainer(myParcelable).encodeDecode {
@@ -315,10 +287,11 @@
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable
data class IBinderContainer(
- @Serializable(with = IBinderSerializer::class) val value: Binder
+ @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
+ @Serializable(with = IBinderSerializer::class)
+ val value: Binder
)
val binder = Binder("foo")
IBinderContainer(binder).encodeDecode {
@@ -333,47 +306,36 @@
@Test
fun collectionTypes() {
@Serializable
+ @Suppress("ArrayInDataClass")
data class CharSequenceArrayContainer(
@Serializable(with = CharSequenceArraySerializer::class)
val value: Array<out CharSequence>
- ) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
- other as CharSequenceArrayContainer
- return value.contentEquals(other.value)
- }
-
- override fun hashCode(): Int {
- return value.contentHashCode()
- }
- }
- val myCharSequenceArray = arrayOf("foo", "bar")
- CharSequenceArrayContainer(myCharSequenceArray).encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getCharSequenceArray("value")).isEqualTo(myCharSequenceArray)
- }
+ )
+ val myCharSequenceArray = arrayOf(StringBuilder("foo"), StringBuilder("bar"))
+ CharSequenceArrayContainer(myCharSequenceArray)
+ .encodeDecode(
+ checkDecoded = { decoded, original -> decoded.value.contentEquals(original.value) },
+ checkEncoded = {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getCharSequenceArray("value")).isEqualTo(myCharSequenceArray)
+ }
+ )
@Serializable
+ @Suppress("ArrayInDataClass")
data class ParcelableArrayContainer(
@Serializable(with = ParcelableArraySerializer::class) val value: Array<out Parcelable>
- ) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
- other as ParcelableArrayContainer
- return value.contentEquals(other.value)
- }
-
- override fun hashCode(): Int {
- return value.contentHashCode()
- }
- }
+ )
val myParcelableArray = arrayOf(MyParcelable(3, "foo", 3.14), MyParcelable(4, "bar", 1.73))
- ParcelableArrayContainer(myParcelableArray).encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getParcelableArray<MyParcelable>("value")).isEqualTo(myParcelableArray)
- }
+ ParcelableArrayContainer(myParcelableArray)
+ .encodeDecode(
+ checkDecoded = { decoded, original -> decoded.value.contentEquals(original.value) },
+ checkEncoded = {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getParcelableArray<MyParcelable>("value"))
+ .isEqualTo(myParcelableArray)
+ }
+ )
@Serializable
data class CharSequenceListContainer(
@@ -406,76 +368,96 @@
append(1, MyParcelable(3, "foo", 3.14))
append(3, MyParcelable(4, "bar", 1.73))
}
- SparseParcelableArrayContainer(mySparseParcelableArray).encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getSparseParcelableArray<Parcelable>("value"))
- .isEqualTo(mySparseParcelableArray)
- }
+ SparseParcelableArrayContainer(mySparseParcelableArray)
+ .encodeDecode(
+ checkDecoded = { decoded, original ->
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ decoded.value.contentEquals(original.value)
+ } else {
+ error("VERSION.SDK_INT < S")
+ }
+ },
+ checkEncoded = {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getSparseParcelableArray<Parcelable>("value"))
+ .isEqualTo(mySparseParcelableArray)
+ }
+ )
}
@Test
fun collectionTypesWithConcreteElement() {
+ @Suppress("ArrayInDataClass")
@Serializable
data class CharSequenceArrayContainer(
- @Serializable(with = CharSequenceArraySerializer::class) val value: Array<String>
- ) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
- other as CharSequenceArrayContainer
- return value.contentEquals(other.value)
- }
-
- override fun hashCode(): Int {
- return value.contentHashCode()
+ @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
+ @Serializable(with = CharSequenceArraySerializer::class)
+ val value: Array<@Serializable(with = CharSequenceSerializer::class) StringBuilder>
+ )
+ val myCharSequenceArray = arrayOf<StringBuilder>(StringBuilder("foo"), StringBuilder("bar"))
+ // `Bundle.getCharSequenceArray()` returns a `CharSequence[]` and the actual element type
+ // is not being retained after parcel/unparcel so the plugin-generated serializer will
+ // get `ClassCastException` when trying to cast it back to `Array<StringBuilder>`.
+ assertThrows(ClassCastException::class) {
+ CharSequenceArrayContainer(myCharSequenceArray).encodeDecode {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getCharSequenceArray("value")).isEqualTo(myCharSequenceArray)
}
}
- val myCharSequenceArray = arrayOf("foo", "bar")
- CharSequenceArrayContainer(myCharSequenceArray).encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getCharSequenceArray("value")).isEqualTo(myCharSequenceArray)
- }
- @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
+ @Suppress("ArrayInDataClass")
@Serializable
data class ParcelableArrayContainer(
@Serializable(with = ParcelableArraySerializer::class)
// Here the serializer for the element is actually not used, but leaving it out leads
// to SERIALIZER_NOT_FOUND compile error.
- val value: Array<@Serializable(with = ParcelableSerializer::class) MyParcelable>
- ) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
- other as ParcelableArrayContainer
- return value.contentEquals(other.value)
- }
-
- override fun hashCode(): Int {
- return value.contentHashCode()
- }
- }
+ val value:
+ Array<@Serializable(with = MyParcelableAsParcelableSerializer::class) MyParcelable>
+ )
val myParcelableArray = arrayOf(MyParcelable(3, "foo", 3.14), MyParcelable(4, "bar", 1.73))
- ParcelableArrayContainer(myParcelableArray).encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getParcelableArray<MyParcelable>("value")).isEqualTo(myParcelableArray)
+ // Even though `Bundle` does retain the actual `Parcelable` type there's no way for us to
+ // specify this `Parcelable` element type for the array, so the restored array is still of
+ // type `Array<Parcelable>` and the plugin-generated serializer will get
+ // `ClassCastException` when trying to cast it back to `Array<MyParcelable>`.
+ assertThrows(ClassCastException::class) {
+ ParcelableArrayContainer(myParcelableArray).encodeDecode {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getParcelableArray<MyParcelable>("value")).isEqualTo(myParcelableArray)
+ }
}
@Serializable
data class CharSequenceListContainer(
- @Serializable(with = CharSequenceListSerializer::class) val value: List<String>
+ @Serializable(with = CharSequenceListSerializer::class)
+ @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
+ val value: List<@Serializable(with = CharSequenceSerializer::class) StringBuilder>
)
- val myCharSequenceList = arrayListOf("foo", "bar")
- CharSequenceListContainer(myCharSequenceList).encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getCharSequenceList("value")).isEqualTo(myCharSequenceList)
- }
+ val myCharSequenceList = arrayListOf(StringBuilder("foo"), StringBuilder("bar"))
- @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
+ CharSequenceListContainer(myCharSequenceList)
+ .encodeDecode(
+ checkDecoded = { decoded, original ->
+ assertThat(original.value[0]::class).isEqualTo(StringBuilder::class)
+ // This is similar to the `CharSequenceArray` case where the element type of the
+ // restored List after parcel/unparcel is of `String` instead of
+ // `StringBuilder`. However, since the element type of Lists is erased no
+ // `CastCastException` is thrown when the plugin-generated serializer tried to
+ // assign the restored list back to `List<StringBuilder>`.
+ assertThat(decoded.value[0]::class).isEqualTo(String::class)
+ },
+ checkEncoded = {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getCharSequenceList("value")).isEqualTo(myCharSequenceList)
+ }
+ )
+
@Serializable
data class ParcelableListContainer(
+ // Unlike arrays this works as `List`s can be down-casted, e.g.
+ // a `List<Parcelable>` can be casted to `List<MyParcelable>`.
@Serializable(with = ParcelableListSerializer::class)
- val value: List<@Serializable(with = ParcelableSerializer::class) MyParcelable>
+ val value:
+ List<@Serializable(with = MyParcelableAsParcelableSerializer::class) MyParcelable>
)
val myParcelableList =
arrayListOf(MyParcelable(3, "foo", 3.14), MyParcelable(4, "bar", 1.73))
@@ -484,57 +466,37 @@
assertThat(getParcelableList<MyParcelable>("value")).isEqualTo(myParcelableList)
}
- @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable
data class SparseParcelableArrayContainer(
+ // Unlike arrays this works as `SparseArray`s can be down-casted, e.g.
+ // a `SparseArray<Parcelable>` can be casted to `SparseArray<MyParcelable>`.
@Serializable(with = SparseParcelableArraySerializer::class)
- val value: SparseArray<@Serializable(with = ParcelableSerializer::class) MyParcelable>
+ val value:
+ SparseArray<
+ @Serializable(with = MyParcelableAsParcelableSerializer::class)
+ MyParcelable
+ >
)
val mySparseParcelableArray =
SparseArray<MyParcelable>().apply {
append(1, MyParcelable(3, "foo", 3.14))
append(3, MyParcelable(4, "bar", 1.73))
}
- SparseParcelableArrayContainer(mySparseParcelableArray).encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getSparseParcelableArray<Parcelable>("value"))
- .isEqualTo(mySparseParcelableArray)
- }
- }
-
- @Test
- fun concreteTypeSerializers() {
- // No need to suppress SERIALIZER_TYPE_INCOMPATIBLE with these serializers.
- @Serializable
- data class CharSequenceContainer(
- @Serializable(with = StringAsCharSequenceSerializer::class) val value: String
- )
- CharSequenceContainer("foo").encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getCharSequence("value")).isEqualTo("foo")
- }
-
- @Serializable
- data class SerializableContainer(
- @Serializable(with = MyJavaSerializableAsJavaSerializableSerializer::class)
- val value: MyJavaSerializable
- )
- val myJavaSerializable = MyJavaSerializable(3, "foo", 3.14)
- SerializableContainer(myJavaSerializable).encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getJavaSerializable<MyJavaSerializable>("value"))
- .isEqualTo(myJavaSerializable)
- }
-
- @Serializable
- data class ParcelableContainer(
- @Serializable(with = MyParcelableAsParcelableSerializer::class) val value: MyParcelable
- )
- val myParcelable = MyParcelable(3, "foo", 3.14)
- ParcelableContainer(myParcelable).encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getParcelable<MyParcelable>("value")).isEqualTo(myParcelable)
- }
+ SparseParcelableArrayContainer(mySparseParcelableArray)
+ .encodeDecode(
+ checkDecoded = { decoded, original ->
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ assertThat(decoded.value.contentEquals(original.value))
+ } else {
+ error("VERSION.SDK_INT < S")
+ }
+ },
+ checkEncoded = {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getSparseParcelableArray<Parcelable>("value"))
+ .isEqualTo(mySparseParcelableArray)
+ }
+ )
}
}
@@ -610,45 +572,11 @@
}
}
-private object CharArrayAsStringSerializer : KSerializer<Array<Char>> {
- private val delegateSerializer = serializer<String>()
- override val descriptor: SerialDescriptor =
- PrimitiveSerialDescriptor("Array<Char>", PrimitiveKind.STRING)
-
- override fun deserialize(decoder: Decoder): Array<Char> {
- val s = decoder.decodeSerializableValue(delegateSerializer)
- val result = Array(s.length) { s[it] }
- return result
- }
-
- override fun serialize(encoder: Encoder, value: Array<Char>) {
- val charArray = CharArray(value.size)
- value.forEachIndexed { index, c -> charArray[index] = c }
- encoder.encodeSerializableValue(delegateSerializer, String(charArray))
- }
-}
-
-@OptIn(ExperimentalSerializationApi::class)
-private object SparseStringArrayAsMapSerializer : KSerializer<SparseArray<String>> {
- private val delegateSerializer = serializer<Map<Int, String>>()
- override val descriptor = SerialDescriptor("SparseArray<String>", delegateSerializer.descriptor)
-
- override fun deserialize(decoder: Decoder): SparseArray<String> {
- val m = decoder.decodeSerializableValue(delegateSerializer)
- val result = SparseArray<String>()
- m.forEach { (k, v) -> result.append(k, v) }
- return result
- }
-
- override fun serialize(encoder: Encoder, value: SparseArray<String>) {
- val map = buildMap { value.forEach { k, v -> put(k, v) } }
- encoder.encodeSerializableValue(delegateSerializer, map)
- }
-}
-
-private class StringAsCharSequenceSerializer : CharSequenceSerializer<String>()
-
private class MyJavaSerializableAsJavaSerializableSerializer :
JavaSerializableSerializer<MyJavaSerializable>()
private class MyParcelableAsParcelableSerializer : ParcelableSerializer<MyParcelable>()
+
+private class CustomJavaSerializableSerializer : JavaSerializableSerializer<java.io.Serializable>()
+
+private class CustomParcelableSerializer : ParcelableSerializer<Parcelable>()
diff --git a/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt b/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt
new file mode 100644
index 0000000..edf79cc
--- /dev/null
+++ b/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.savedstate
+
+import android.os.Parcel
+
+actual fun platformEncodeDecode(savedState: SavedState): SavedState {
+ val parcel =
+ Parcel.obtain().apply {
+ savedState.writeToParcel(this, 0)
+ setDataPosition(0)
+ }
+ return SavedState.CREATOR.createFromParcel(parcel)
+}
diff --git a/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTest.kt b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTest.kt
index 86adf0b..89d6548 100644
--- a/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTest.kt
+++ b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTest.kt
@@ -464,19 +464,24 @@
putSavedState("ss", savedState { putString("s", "bar") })
}
)
- .encodeDecode {
- assertThat(size()).isEqualTo(1)
- getSavedState("s").read {
- assertThat(size()).isEqualTo(4)
- assertThat(getInt("i")).isEqualTo(1)
- assertThat(getString("s")).isEqualTo("foo")
- assertThat(getIntArray("a")).isEqualTo(intArrayOf(1, 3, 5))
- getSavedState("ss").read {
- assertThat(size()).isEqualTo(1)
- assertThat(getString("s")).isEqualTo("bar")
+ .encodeDecode(
+ checkDecoded = { decoded, original ->
+ assertThat(decoded.s.read { contentDeepEquals(original.s) })
+ },
+ checkEncoded = {
+ assertThat(size()).isEqualTo(1)
+ getSavedState("s").read {
+ assertThat(size()).isEqualTo(4)
+ assertThat(getInt("i")).isEqualTo(1)
+ assertThat(getString("s")).isEqualTo("foo")
+ assertThat(getIntArray("a")).isEqualTo(intArrayOf(1, 3, 5))
+ getSavedState("ss").read {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getString("s")).isEqualTo("bar")
+ }
}
}
- }
+ )
val origin = savedState {
putInt("i", 1)
diff --git a/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.kt b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.kt
index e9ac161..67930cd 100644
--- a/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.kt
+++ b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.kt
@@ -23,16 +23,30 @@
import kotlinx.serialization.serializer
internal object SavedStateCodecTestUtils {
+ /* Test the following steps: 1. encode `T` to a `SavedState`, 2. parcelize it to a `Parcel`,
+ * 3. un-parcelize it back to a `SavedState`, and 4. decode it back to a `T`. Step 2 and 3
+ * are only performed on Android. Here's the whole process:
+ *
+ * (A)Serializable -1-> (B)SavedState -2-> (C)Parcel -3-> (D)SavedState -4-> (E)Serializable
+ *
+ * `checkEncoded` can be used to check the content of "B", and `checkDecoded` can be
+ * used to compare the instances of "E" and "A".
+ */
inline fun <reified T : Any> T.encodeDecode(
serializer: KSerializer<T> = serializer<T>(),
- checkContent: SavedStateReader.() -> Unit = { assertThat(size()).isEqualTo(0) }
+ checkDecoded: (T, T) -> Unit = { decoded, original ->
+ assertThat(decoded).isEqualTo(original)
+ },
+ checkEncoded: SavedStateReader.() -> Unit = { assertThat(size()).isEqualTo(0) }
) {
- assertThat(
- decodeFromSavedState(
- serializer,
- encodeToSavedState(serializer, this).apply { read { checkContent() } }
- )
- )
- .isEqualTo(this)
+ val encoded = encodeToSavedState(serializer, this)
+ encoded.read { checkEncoded() }
+
+ val restored = platformEncodeDecode(encoded)
+
+ val decoded = decodeFromSavedState(serializer, restored)
+ checkDecoded(decoded, this)
}
}
+
+expect fun platformEncodeDecode(savedState: SavedState): SavedState
diff --git a/savedstate/savedstate/src/nonAndroidTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.nonAndroid.kt b/savedstate/savedstate/src/nonAndroidTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.nonAndroid.kt
new file mode 100644
index 0000000..023701a8
--- /dev/null
+++ b/savedstate/savedstate/src/nonAndroidTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.nonAndroid.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.savedstate
+
+// No parceling in non-Android platforms.
+actual fun platformEncodeDecode(savedState: SavedState): SavedState = savedState