Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Bundle#putSerializable and Bundle#getSerializable extensions #166

Merged
merged 1 commit into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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())
}
Loading