Add platform-specific encoding and decoding to SavedState codec tests
Add platform-specific encoding and decoding (parcelling and unparcelling on Android and no-op on other platforms) to SavedState codec tests for increased coverage.
After this change, we got `ClassCastException` when restoring arrays of sub-type of `Parcelable`s. This is because the array restored is of type `Array<Parcelable>` and we cannot assign it to an array of Parcelable's subtype, e.g. `Array<MyParcelable>`. I haven't found a way to create instances of `Array<subtype of Parcelable>` in our serializer so I changed the tests to reflect that using `ParcelableArraySerializer` with `Array<subtype of Parcelable>` will lead to `ClassCastException`. Now in all our built-in serializers for collection types, only `ParcelableListSerializer` and `SparseParcelableArraySerializer` can be used with subtypes.
A `checkDecoded` parameter is added to `encodeDecode()`. This can be used to check that the decoded Serializable is essentially the same as the original Serializable when its `equals()` doesn't work.
Bug: 387270265
Test: SavedStateCodecAndroidTest.android.kt↲
Change-Id: I31beee5105f1a6a5495e24f6c9e643e8550bc6cc
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 54e07b7..0f013b0 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
@@ -25,6 +25,7 @@
import android.util.SparseArray
import androidx.savedstate.SavedState
import androidx.savedstate.read
+import androidx.savedstate.savedState
import androidx.savedstate.serialization.SavedStateDecoder
import androidx.savedstate.serialization.SavedStateEncoder
import androidx.savedstate.write
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 0536f05..ce4bdb9 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 {
@@ -306,7 +279,7 @@
@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 {
@@ -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(
@@ -443,29 +405,24 @@
}
}
- @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
+ @Suppress("SERIALIZER_TYPE_INCOMPATIBLE", "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 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
@@ -496,6 +453,8 @@
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@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>
)
@@ -509,6 +468,8 @@
@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>
)
@@ -517,46 +478,21 @@
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)
+ }
+ )
}
}
@@ -632,45 +568,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