Skip to content

Commit

Permalink
Merge pull request #117 from arkivanov/polymorphic-serializer
Browse files Browse the repository at this point in the history
Added polymorphicSerializer API
  • Loading branch information
arkivanov authored Nov 5, 2023
2 parents a3b0e8c + 7677d31 commit 52a1eea
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 1 deletion.
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ lifecycleRegistry.destroy()

## Parcelable and Parcelize (deprecated since v1.3.0-alpha01)

> ⚠️ Unfortunatelly, the new K2 compiler will not support Parcelable/Parcelize with Kotlin Multiplatform (see [#102](https://github.com/arkivanov/Essenty/issues/102)). This module is mostly deprecated since `v1.3.0-alpha01` and will be removed in `v2.0`. As a replacement, Essenty supports [kotlinx-serialization](https://github.com/Kotlin/kotlinx.serialization) since `v1.3.0-alpha01`.
> ⚠️ Unfortunately, the new K2 compiler will not support Parcelable/Parcelize with Kotlin Multiplatform (see [#102](https://github.com/arkivanov/Essenty/issues/102)). This module is mostly deprecated since `v1.3.0-alpha01` and will be removed in `v2.0`. As a replacement, Essenty supports [kotlinx-serialization](https://github.com/Kotlin/kotlinx.serialization) since `v1.3.0-alpha01`.
Essenty brings both [Android Parcelable](https://developer.android.com/reference/android/os/Parcelable) interface and the `@Parcelize` annotation from [kotlin-parcelize](https://developer.android.com/kotlin/parcelize) compiler plugin to Kotlin Multiplatform, so they both can be used in common code. This is typically used for state/data preservation over [Android configuration changes](https://developer.android.com/guide/topics/resources/runtime-changes), when writing common code targeting Android.

Expand Down Expand Up @@ -352,6 +352,41 @@ class SomeLogic(stateKeeper: StateKeeper) {
}
```

##### Polymorphic serialization (experimental)

Sometimes it might be necessary to serialize an interface or an abstract class that you don't own but have implemented. For this purpose Essenty provides `polymorphicSerializer` function that can be used to create custom polymorphic serializers for unowned base types.

For example a third-party library may have the following interface.

```kotlin
interface Filter {
// Omitted code
}
```

Then we can have multiple implementations of `Filter`.

```kotlin
@Serializable
class TextFilter(val text: String) : Filter { /* Omitted code */ }

@Serializable
class RatingFilter(val stars: Int) : Filter { /* Omitted code */ }
```

Now we can create a polymorphic serializer for `Filter` as follows. It can be used to save and restore `Filter` directly via StateKeeper, or to have `Filter` as part of another `Serializable` class.

```kotlin
object FilterSerializer : KSerializer<Filter> by polymorphicSerializer(
SerializersModule {
polymorphic(Filter::class) {
subclass(TextFilter::class, TextFilter.serializer())
subclass(RatingFilter::class, RatingFilter.serializer())
}
}
)
```

#### Using StateKeeper (the deprecated Parcelable way before v1.3.0-alpha01)

```kotlin
Expand Down
4 changes: 4 additions & 0 deletions state-keeper/api/android/state-keeper.api
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ public final class com/arkivanov/essenty/statekeeper/DefaultStateKeeperDispatche
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/arkivanov/essenty/statekeeper/PolymorphicSerializerKt {
public static final fun polymorphicSerializer (Lkotlin/reflect/KClass;Lkotlinx/serialization/modules/SerializersModule;)Lkotlinx/serialization/KSerializer;
}

public abstract interface class com/arkivanov/essenty/statekeeper/StateKeeper {
public abstract fun consume (Ljava/lang/String;Lkotlin/reflect/KClass;)Landroid/os/Parcelable;
public abstract fun consume (Ljava/lang/String;Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object;
Expand Down
4 changes: 4 additions & 0 deletions state-keeper/api/jvm/state-keeper.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
public final class com/arkivanov/essenty/statekeeper/PolymorphicSerializerKt {
public static final fun polymorphicSerializer (Lkotlin/reflect/KClass;Lkotlinx/serialization/modules/SerializersModule;)Lkotlinx/serialization/KSerializer;
}

public abstract interface class com/arkivanov/essenty/statekeeper/StateKeeper {
public abstract fun consume (Ljava/lang/String;Lkotlin/reflect/KClass;)Lcom/arkivanov/essenty/parcelable/Parcelable;
public abstract fun consume (Ljava/lang/String;Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.arkivanov.essenty.statekeeper

import com.arkivanov.essenty.utils.internal.ExperimentalEssentyApi
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.SerialKind
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.descriptors.element
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.encoding.encodeStructure
import kotlinx.serialization.modules.SerializersModule
import kotlin.reflect.KClass

/**
* Creates a polymorphic [KSerializer] for the specified class of type [T] using the specified [module].
*/
@ExperimentalEssentyApi
@ExperimentalSerializationApi
inline fun <reified T : Any> polymorphicSerializer(module: SerializersModule) : KSerializer<T> =
polymorphicSerializer(baseClass = T::class, module = module)

/**
* Creates a polymorphic [KSerializer] for the specified [baseClass] class using the specified [module].
*/
@ExperimentalEssentyApi
@ExperimentalSerializationApi
fun <T : Any> polymorphicSerializer(baseClass: KClass<T>, module: SerializersModule) : KSerializer<T> =
PolymorphicSerializer(baseClass = baseClass, module = module)

@ExperimentalSerializationApi
private class PolymorphicSerializer<T : Any>(
private val baseClass: KClass<T>,
private val module: SerializersModule,
) : KSerializer<T> {
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("PolymorphicSerializer") {
element<String>("type")
element("value", ContextualSerialDescriptor)
}

override fun serialize(encoder: Encoder, value: T) {
val serializer = requireNotNull(module.getPolymorphic(baseClass, value))
encoder.encodeStructure(descriptor) {
encodeStringElement(descriptor, 0, serializer.descriptor.serialName)
encodeSerializableElement(descriptor, 1, serializer, value)
}
}

override fun deserialize(decoder: Decoder): T =
decoder.decodeStructure(descriptor) {
var className: String? = null
var value: T? = null

while (true) {
when (val index = decodeElementIndex(descriptor)) {
0 -> className = decodeStringElement(descriptor, index)

1 -> {
val actualClassName = requireNotNull(className)
val serializer = requireNotNull(module.getPolymorphic(baseClass, actualClassName))
value = decodeSerializableElement(descriptor, 1, serializer)
}

CompositeDecoder.DECODE_DONE -> break

else -> error("Unsupported index: $index")
}
}

requireNotNull(value)
}

private object ContextualSerialDescriptor : SerialDescriptor {
override val elementsCount: Int = 0
override val kind: SerialKind = SerialKind.CONTEXTUAL
override val serialName: String = "Value"

override fun getElementAnnotations(index: Int): List<Annotation> = elementNotFoundError(index)
override fun getElementDescriptor(index: Int): SerialDescriptor = elementNotFoundError(index)
override fun getElementIndex(name: String): Int = CompositeDecoder.UNKNOWN_NAME
override fun getElementName(index: Int): String = elementNotFoundError(index)
override fun isElementOptional(index: Int): Boolean = elementNotFoundError(index)

private fun elementNotFoundError(index: Int): Nothing {
throw IndexOutOfBoundsException("Element at index $index not found")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.arkivanov.essenty.statekeeper

import com.arkivanov.essenty.utils.internal.ExperimentalEssentyApi
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlin.test.Test
import kotlin.test.assertEquals

class PolymorphicSerializerTest {

@Test
fun serialize_and_deserialize() {
val someListSerializer = ListSerializer(SomeSerializer)
val originalSome = listOf(Some1(1), Some2("2"))

val newSome = originalSome.serialize(someListSerializer).deserialize(someListSerializer)

assertEquals(originalSome, newSome)
}

private interface Some

@Serializable
private data class Some1(val value: Int) : Some

@Serializable
private data class Some2(val value: String) : Some

@OptIn(ExperimentalEssentyApi::class, ExperimentalSerializationApi::class)
private object SomeSerializer : KSerializer<Some> by polymorphicSerializer(
SerializersModule {
polymorphic(Some::class) {
subclass(Some1::class, Some1.serializer())
subclass(Some2::class, Some2.serializer())
}
}
)
}

0 comments on commit 52a1eea

Please sign in to comment.