From 44fb7b147cd170acc935ec0ce3a3d54d2fbd9505 Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Sun, 14 Apr 2024 18:28:18 +0100 Subject: [PATCH] Added Bundle#putSerializable and Bundle#getSerializable extensions --- state-keeper/api/android/state-keeper.api | 2 + .../essenty/statekeeper/BundleExt.kt | 62 ++++++++++++++----- .../statekeeper/AndroidStateKeeperTest.kt | 14 ----- .../essenty/statekeeper/BundleExtTest.kt | 48 ++++++++++++++ .../essenty/statekeeper/TestUtils.android.kt | 18 ++++++ 5 files changed, 113 insertions(+), 31 deletions(-) create mode 100644 state-keeper/src/androidUnitTest/kotlin/com/arkivanov/essenty/statekeeper/BundleExtTest.kt create mode 100644 state-keeper/src/androidUnitTest/kotlin/com/arkivanov/essenty/statekeeper/TestUtils.android.kt diff --git a/state-keeper/api/android/state-keeper.api b/state-keeper/api/android/state-keeper.api index 369a5be..bc56188 100644 --- a/state-keeper/api/android/state-keeper.api +++ b/state-keeper/api/android/state-keeper.api @@ -6,7 +6,9 @@ public final class com/arkivanov/essenty/statekeeper/AndroidExtKt { } public final class com/arkivanov/essenty/statekeeper/BundleExtKt { + public static final fun getSerializable (Landroid/os/Bundle;Ljava/lang/String;Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object; public static final fun getSerializableContainer (Landroid/os/Bundle;Ljava/lang/String;)Lcom/arkivanov/essenty/statekeeper/SerializableContainer; + public static final fun putSerializable (Landroid/os/Bundle;Ljava/lang/String;Ljava/lang/Object;Lkotlinx/serialization/SerializationStrategy;)V public static final fun putSerializableContainer (Landroid/os/Bundle;Ljava/lang/String;Lcom/arkivanov/essenty/statekeeper/SerializableContainer;)V } diff --git a/state-keeper/src/androidMain/kotlin/com/arkivanov/essenty/statekeeper/BundleExt.kt b/state-keeper/src/androidMain/kotlin/com/arkivanov/essenty/statekeeper/BundleExt.kt index 0e61841..3d0272b 100644 --- a/state-keeper/src/androidMain/kotlin/com/arkivanov/essenty/statekeeper/BundleExt.kt +++ b/state-keeper/src/androidMain/kotlin/com/arkivanov/essenty/statekeeper/BundleExt.kt @@ -3,45 +3,73 @@ package com.arkivanov.essenty.statekeeper import android.os.Bundle import android.os.Parcel import android.os.Parcelable +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.SerializationStrategy /** - * Inserts the provided [SerializableContainer] into this [Bundle], - * replacing any existing value for the given [key]. Either [key] or [value] may be `null`. + * Inserts the provided `kotlinx-serialization` [Serializable][kotlinx.serialization.Serializable] value + * into this [Bundle], replacing any existing value for the given [key]. + * Either [key] or [value] may be `null`. */ -fun Bundle.putSerializableContainer(key: String?, value: SerializableContainer?) { - putParcelable(key, value?.let(::SerializableContainerWrapper)) +fun Bundle.putSerializable(key: String?, value: T?, strategy: SerializationStrategy) { + putParcelable(key, SerializableHolder(value = value to strategy, bytes = null)) } /** - * Returns a [SerializableContainer] associated with the given [key], - * or `null` if no mapping exists for the given [key] or a `null` value - * is explicitly associated with the [key]. + * Returns a `kotlinx-serialization` [Serializable][kotlinx.serialization.Serializable] associated with + * the given [key], or `null` if no mapping exists for the given [key] or a `null` value is explicitly + * associated with the [key]. */ +fun Bundle.getSerializable(key: String?, strategy: DeserializationStrategy): T? = + getParcelableCompat>(key)?.let { value -> + value.value?.first ?: value.bytes?.deserialize(strategy) + } + @Suppress("DEPRECATION") -fun Bundle.getSerializableContainer(key: String?): SerializableContainer? = +private inline fun Bundle.getParcelableCompat(key: String?): T? = classLoader.let { savedClassLoader -> try { - classLoader = SerializableContainerWrapper::class.java.classLoader - (getParcelable(key) as SerializableContainerWrapper?)?.container + classLoader = T::class.java.classLoader + getParcelable(key) as T? } finally { classLoader = savedClassLoader } } -private class SerializableContainerWrapper( - val container: SerializableContainer, +/** + * Inserts the provided [SerializableContainer] into this [Bundle], + * replacing any existing value for the given [key]. Either [key] or [value] may be `null`. + */ +fun Bundle.putSerializableContainer(key: String?, value: SerializableContainer?) { + putSerializable(key = key, value = value, strategy = SerializableContainer.serializer()) +} + +/** + * Returns a [SerializableContainer] associated with the given [key], + * or `null` if no mapping exists for the given [key] or a `null` value + * is explicitly associated with the [key]. + */ +fun Bundle.getSerializableContainer(key: String?): SerializableContainer? = + getSerializable(key = key, strategy = SerializableContainer.serializer()) + +private class SerializableHolder( + val value: Pair>?, + val bytes: ByteArray?, ) : Parcelable { override fun writeToParcel(dest: Parcel, flags: Int) { - dest.writeByteArray(container.serialize(strategy = SerializableContainer.serializer())) + dest.writeByteArray(bytes ?: value?.serialize()) } + private fun Pair>.serialize(): ByteArray? = + first?.serialize(second) + override fun describeContents(): Int = 0 - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): SerializableContainerWrapper = - SerializableContainerWrapper(requireNotNull(parcel.createByteArray()).deserialize(SerializableContainer.serializer())) + companion object CREATOR : Parcelable.Creator> { + override fun createFromParcel(parcel: Parcel): SerializableHolder = + SerializableHolder(value = null, bytes = parcel.createByteArray()) - override fun newArray(size: Int): Array = + override fun newArray(size: Int): Array?> = arrayOfNulls(size) } } diff --git a/state-keeper/src/androidUnitTest/kotlin/com/arkivanov/essenty/statekeeper/AndroidStateKeeperTest.kt b/state-keeper/src/androidUnitTest/kotlin/com/arkivanov/essenty/statekeeper/AndroidStateKeeperTest.kt index 2a35710..66c99c5 100644 --- a/state-keeper/src/androidUnitTest/kotlin/com/arkivanov/essenty/statekeeper/AndroidStateKeeperTest.kt +++ b/state-keeper/src/androidUnitTest/kotlin/com/arkivanov/essenty/statekeeper/AndroidStateKeeperTest.kt @@ -80,20 +80,6 @@ class AndroidStateKeeperTest { assertNull(restoredData) } - private fun Bundle.parcelize(): ByteArray { - val parcel = Parcel.obtain() - parcel.writeBundle(this) - return parcel.marshall() - } - - private fun ByteArray.deparcelize(): Bundle { - val parcel = Parcel.obtain() - parcel.unmarshall(this, 0, size) - parcel.setDataPosition(0) - - return requireNotNull(parcel.readBundle()) - } - private class TestSavedStateRegistryOwner : SavedStateRegistryOwner { val controller: SavedStateRegistryController = SavedStateRegistryController.create(this) diff --git a/state-keeper/src/androidUnitTest/kotlin/com/arkivanov/essenty/statekeeper/BundleExtTest.kt b/state-keeper/src/androidUnitTest/kotlin/com/arkivanov/essenty/statekeeper/BundleExtTest.kt new file mode 100644 index 0000000..a043961 --- /dev/null +++ b/state-keeper/src/androidUnitTest/kotlin/com/arkivanov/essenty/statekeeper/BundleExtTest.kt @@ -0,0 +1,48 @@ +package com.arkivanov.essenty.statekeeper + +import android.os.Bundle +import kotlinx.serialization.Serializable +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.Test +import kotlin.test.assertEquals + +@RunWith(RobolectricTestRunner::class) +class BundleExtTest { + + @Test + fun getSerializable_returns_same_value_after_putSerializable_without_serialization() { + val value = Value(value = "123") + val bundle = Bundle() + bundle.putSerializable(key = "key", value = value, strategy = Value.serializer()) + val newValue = bundle.getSerializable(key = "key", strategy = Value.serializer()) + + assertEquals(value, newValue) + } + + @Test + fun getSerializable_returns_same_value_after_putSerializable_with_serialization() { + val value = Value(value = "123") + val bundle = Bundle() + bundle.putSerializable(key = "key", value = value, strategy = Value.serializer()) + val newValue = bundle.parcelize().deparcelize().getSerializable(key = "key", strategy = Value.serializer()) + + assertEquals(value, newValue) + } + + @Test + fun getSerializable_returns_same_value_after_putSerializable_with_double_serialization() { + val value = Value(value = "123") + val bundle = Bundle() + bundle.putSerializable(key = "key", value = value, strategy = Value.serializer()) + bundle.putInt("int", 123) + val newBundle = bundle.parcelize().deparcelize() + newBundle.getInt("int") // Force partial deserialization of the Bundle + val newValue = newBundle.parcelize().deparcelize().getSerializable(key = "key", strategy = Value.serializer()) + + assertEquals(value, newValue) + } + + @Serializable + data class Value(val value: String) +} diff --git a/state-keeper/src/androidUnitTest/kotlin/com/arkivanov/essenty/statekeeper/TestUtils.android.kt b/state-keeper/src/androidUnitTest/kotlin/com/arkivanov/essenty/statekeeper/TestUtils.android.kt new file mode 100644 index 0000000..368d466 --- /dev/null +++ b/state-keeper/src/androidUnitTest/kotlin/com/arkivanov/essenty/statekeeper/TestUtils.android.kt @@ -0,0 +1,18 @@ +package com.arkivanov.essenty.statekeeper + +import android.os.Bundle +import android.os.Parcel + +internal fun Bundle.parcelize(): ByteArray { + val parcel = Parcel.obtain() + parcel.writeBundle(this) + return parcel.marshall() +} + +internal fun ByteArray.deparcelize(): Bundle { + val parcel = Parcel.obtain() + parcel.unmarshall(this, 0, size) + parcel.setDataPosition(0) + + return requireNotNull(parcel.readBundle()) +}