Skip to content

Commit

Permalink
Merge pull request #166 from arkivanov/putSerializable
Browse files Browse the repository at this point in the history
Added Bundle#putSerializable and Bundle#getSerializable extensions
  • Loading branch information
arkivanov authored Apr 17, 2024
2 parents 3ee3fc0 + a518bed commit f594e56
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 31 deletions.
2 changes: 2 additions & 0 deletions state-keeper/api/android/state-keeper.api
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,70 @@ 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 <T : Any> Bundle.putSerializable(key: String?, value: T?, strategy: SerializationStrategy<T>) {
putParcelable(key, ValueHolder(value = value, bytes = lazy { value?.serialize(strategy) }))
}

/**
* 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 <T : Any> Bundle.getSerializable(key: String?, strategy: DeserializationStrategy<T>): T? =
getParcelableCompat<ValueHolder<T>>(key)?.let { holder ->
holder.value ?: holder.bytes.value?.deserialize(strategy)
}

@Suppress("DEPRECATION")
fun Bundle.getSerializableContainer(key: String?): SerializableContainer? =
private inline fun <reified T : Parcelable> 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 ValueHolder<out T : Any>(
val value: T?,
val bytes: Lazy<ByteArray?>,
) : Parcelable {
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeByteArray(container.serialize(strategy = SerializableContainer.serializer()))
dest.writeByteArray(bytes.value)
}

override fun describeContents(): Int = 0

companion object CREATOR : Parcelable.Creator<SerializableContainerWrapper> {
override fun createFromParcel(parcel: Parcel): SerializableContainerWrapper =
SerializableContainerWrapper(requireNotNull(parcel.createByteArray()).deserialize(SerializableContainer.serializer()))
companion object CREATOR : Parcelable.Creator<ValueHolder<Any>> {
override fun createFromParcel(parcel: Parcel): ValueHolder<Any> =
ValueHolder(value = null, bytes = lazyOf(parcel.createByteArray()))

override fun newArray(size: Int): Array<SerializableContainerWrapper?> =
override fun newArray(size: Int): Array<ValueHolder<Any>?> =
arrayOfNulls(size)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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())
}

0 comments on commit f594e56

Please sign in to comment.