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

Fixed Serialization Issue #9 #11

Merged
merged 7 commits into from
Jul 12, 2023
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
5 changes: 2 additions & 3 deletions navigation-compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,8 @@ kotlin {
implementation(compose.ui)
implementation(compose.foundation)

implementation(KotlinX.serialization.core)
implementation("com.chrynan.parcelable:parcelable-core:_")
implementation("com.chrynan.parcelable:parcelable-compose:_")
api("com.chrynan.parcelable:parcelable-core:_")
api("com.chrynan.parcelable:parcelable-compose:_")
}
}
if (isBuildingOnOSX()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ fun <Destination : NavigationDestination, Context : NavigationContext<Destinatio
inline fun <reified Destination : NavigationDestination> rememberSavableNavigator(
initialDestination: Destination,
parcelable: Parcelable = Parcelable.Default,
destinationSerializer: KSerializer<Destination> = Parcelable.serializersModule.serializer(),
destinationSerializer: KSerializer<Destination> = parcelable.serializersModule.serializer(),
duplicateDestinationStrategy: NavigationStrategy.DuplicateDestination = NavigationStrategy.DuplicateDestination.ALLOW_DUPLICATES,
backwardsNavigationStrategy: NavigationStrategy.BackwardsNavigation = NavigationStrategy.BackwardsNavigation.IN_CONTEXT,
destinationRetentionStrategy: NavigationStrategy.DestinationRetention = NavigationStrategy.DestinationRetention.RETAIN
Expand Down Expand Up @@ -210,8 +210,8 @@ inline fun <reified Destination : NavigationDestination> rememberSavableNavigato
inline fun <reified Destination : NavigationDestination, reified Context : NavigationContext<Destination>> rememberSavableNavigator(
initialContext: Context,
parcelable: Parcelable = Parcelable.Default,
destinationSerializer: KSerializer<Destination> = Parcelable.serializersModule.serializer(),
contextSerializer: KSerializer<Context> = Parcelable.serializersModule.serializer(),
destinationSerializer: KSerializer<Destination> = parcelable.serializersModule.serializer(),
contextSerializer: KSerializer<Context> = parcelable.serializersModule.serializer(),
duplicateDestinationStrategy: NavigationStrategy.DuplicateDestination = NavigationStrategy.DuplicateDestination.ALLOW_DUPLICATES,
backwardsNavigationStrategy: NavigationStrategy.BackwardsNavigation = NavigationStrategy.BackwardsNavigation.IN_CONTEXT,
destinationRetentionStrategy: NavigationStrategy.DestinationRetention = NavigationStrategy.DestinationRetention.RETAIN
Expand Down
2 changes: 1 addition & 1 deletion navigation-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ kotlin {
implementation(Kotlin.stdlib.common)

implementation(KotlinX.coroutines.core)
implementation(KotlinX.serialization.core)
api(KotlinX.serialization.core)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

package com.chrynan.navigation

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

/**
* Represents a navigation event that is sent to a [Navigator] to coordinate the navigation between UI components, such
Expand All @@ -14,7 +16,7 @@ import kotlinx.serialization.Serializable
*
* @see [Navigator.dispatch]
*/
@Serializable
@Serializable(with = NavigationEventSerializer::class)
sealed class NavigationEvent<D : NavigationDestination, C : NavigationContext<D>> private constructor() {

/**
Expand All @@ -29,6 +31,16 @@ sealed class NavigationEvent<D : NavigationDestination, C : NavigationContext<D>
*/
abstract val direction: Direction

/**
* The type of [NavigationEvent] that occurred.
*/
abstract val type: Type

/**
* Creates a serializable [Snapshot] instance of this [NavigationEvent].
*/
internal abstract fun toSnapshot(): Snapshot<D, C>

/**
* Represents a direction for a [NavigationEvent]. A [NavigationEvent] can either be a [FORWARDS] direction event,
* meaning the change is added to a [Stack], or a [BACKWARDS] direction event, meaning the change causes a removal
Expand All @@ -50,6 +62,28 @@ sealed class NavigationEvent<D : NavigationDestination, C : NavigationContext<D>
FORWARDS(serialName = "forwards")
}

@Serializable
enum class Type(val serialName: String) {

/**
* Corresponds to the [NavigationEvent.Backward] type.
*/
@SerialName(value = "backwards")
BACKWARDS(serialName = "backwards"),

/**
* Corresponds to the [NavigationEvent.Forward.Context] type.
*/
@SerialName(value = "context")
CONTEXT(serialName = "context"),

/**
* Corresponds to the [NavigationEvent.Forward.Destination] type.
*/
@SerialName(value = "destination")
DESTINATION(serialName = "destination")
}

/**
* A [NavigationEvent] that represents a reversal of a previous navigation event. A [Backward] navigation event can be
* used to go to the previous [NavigationDestination] within the current [NavigationContext], or going back to a
Expand All @@ -59,13 +93,22 @@ sealed class NavigationEvent<D : NavigationDestination, C : NavigationContext<D>
* occurred. **Note:** This is not safe to persist or use between system reboots.
*/
@Serializable
@SerialName(value = "back")
@SerialName(value = "backwards")
class Backward<D : NavigationDestination, C : NavigationContext<D>> internal constructor(
@SerialName(value = "instant") override val elapsedMilliseconds: Long = elapsedSystemTime().inWholeMilliseconds
) : NavigationEvent<D, C>() {

override val direction: Direction = Direction.BACKWARDS

@Transient
override val type: Type = Type.BACKWARDS

override fun toSnapshot(): Snapshot<D, C> = Snapshot(
type = type,
direction = direction,
elapsedMilliseconds = elapsedMilliseconds
)

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Backward<*, *>) return false
Expand All @@ -85,7 +128,7 @@ sealed class NavigationEvent<D : NavigationDestination, C : NavigationContext<D>
* reversed by a [Backward] event. There are two forward navigation events: [Destination] representing a change in
* destination on the current context stack, and [Context] representing a change in the context.
*/
@Serializable
@Serializable(with = NavigationEventForwardSerializer::class)
@SerialName(value = "forward")
sealed class Forward<D : NavigationDestination, C : NavigationContext<D>> private constructor() :
NavigationEvent<D, C>() {
Expand All @@ -107,6 +150,16 @@ sealed class NavigationEvent<D : NavigationDestination, C : NavigationContext<D>
@SerialName(value = "destination") val destination: D
) : Forward<D, C>() {

@Transient
override val type: Type = Type.DESTINATION

override fun toSnapshot(): Snapshot<D, C> = Snapshot(
type = type,
direction = direction,
elapsedMilliseconds = elapsedMilliseconds,
destination = destination
)

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Destination<*, *>) return false
Expand Down Expand Up @@ -140,6 +193,16 @@ sealed class NavigationEvent<D : NavigationDestination, C : NavigationContext<D>
@SerialName(value = "context") val context: C
) : Forward<D, C>() {

@Transient
override val type: Type = Type.CONTEXT

override fun toSnapshot(): Snapshot<D, C> = Snapshot(
type = type,
direction = direction,
elapsedMilliseconds = elapsedMilliseconds,
context = context
)

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Context<*, *>) return false
Expand All @@ -160,5 +223,161 @@ sealed class NavigationEvent<D : NavigationDestination, C : NavigationContext<D>
}
}

@Serializable
internal class Snapshot<Destination : NavigationDestination, Context : NavigationContext<Destination>>(
@SerialName(value = "type") val type: Type,
@SerialName(value = "direction") val direction: Direction,
@SerialName(value = "elapsed_milliseconds") val elapsedMilliseconds: Long,
@SerialName(value = "destination") val destination: Destination? = null,
@SerialName(value = "context") val context: Context? = null
) {

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Snapshot<*, *>) return false

if (type != other.type) return false
if (direction != other.direction) return false
if (elapsedMilliseconds != other.elapsedMilliseconds) return false
if (destination != other.destination) return false

return context == other.context
}

override fun hashCode(): Int {
var result = type.hashCode()
result = 31 * result + direction.hashCode()
result = 31 * result + elapsedMilliseconds.hashCode()
result = 31 * result + (destination?.hashCode() ?: 0)
result = 31 * result + (context?.hashCode() ?: 0)
return result
}

override fun toString(): String =
"Snapshot(" +
"type=$type, " +
"direction=$direction, " +
"elapsedMilliseconds=$elapsedMilliseconds, " +
"destination=$destination, " +
"context=$context)"
}

companion object
}

/**
* Creates a [NavigationEvent] instance with the provided [NavigationEvent.Snapshot].
*/
internal fun <Destination : NavigationDestination, Context : NavigationContext<Destination>> NavigationEvent(
snapshot: NavigationEvent.Snapshot<Destination, Context>
): NavigationEvent<Destination, Context> =
when (snapshot.type) {
NavigationEvent.Type.BACKWARDS -> NavigationEvent.Backward(elapsedMilliseconds = snapshot.elapsedMilliseconds)
NavigationEvent.Type.DESTINATION -> NavigationEvent.Forward.Destination(
elapsedMilliseconds = snapshot.elapsedMilliseconds,
destination = snapshot.destination!!
)

NavigationEvent.Type.CONTEXT -> NavigationEvent.Forward.Context(
elapsedMilliseconds = snapshot.elapsedMilliseconds,
context = snapshot.context!!
)
}

internal class NavigationEventSerializer<Destination : NavigationDestination, Context : NavigationContext<Destination>>(
destinationSerializer: KSerializer<Destination>,
contextSerializer: KSerializer<Context>
) : KSerializer<NavigationEvent<Destination, Context>> {

private val delegateSerializer = NavigationEvent.Snapshot.serializer(
destinationSerializer,
contextSerializer
)

override val descriptor: SerialDescriptor = destinationSerializer.descriptor

override fun serialize(encoder: Encoder, value: NavigationEvent<Destination, Context>) {
encoder.encodeSerializableValue(
serializer = delegateSerializer,
value = value.toSnapshot()
)
}

override fun deserialize(decoder: Decoder): NavigationEvent<Destination, Context> {
val snapshot = decoder.decodeSerializableValue(deserializer = delegateSerializer)

return NavigationEvent(snapshot = snapshot)
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is NavigationEventSerializer<*, *>) return false

if (delegateSerializer != other.delegateSerializer) return false

return descriptor == other.descriptor
}

override fun hashCode(): Int {
var result = delegateSerializer.hashCode()
result = 31 * result + descriptor.hashCode()
return result
}

override fun toString(): String =
"NavigationEventSerializer(" +
"delegateSerializer=$delegateSerializer, " +
"descriptor=$descriptor)"
}

internal class NavigationEventForwardSerializer<Destination : NavigationDestination, Context : NavigationContext<Destination>>(
destinationSerializer: KSerializer<Destination>,
contextSerializer: KSerializer<Context>
) : KSerializer<NavigationEvent.Forward<Destination, Context>> {

private val delegateSerializer = NavigationEvent.Snapshot.serializer(
destinationSerializer,
contextSerializer
)

override val descriptor: SerialDescriptor = destinationSerializer.descriptor

override fun serialize(encoder: Encoder, value: NavigationEvent.Forward<Destination, Context>) {
encoder.encodeSerializableValue(
serializer = delegateSerializer,
value = value.toSnapshot()
)
}

override fun deserialize(decoder: Decoder): NavigationEvent.Forward<Destination, Context> {
val snapshot = decoder.decodeSerializableValue(deserializer = delegateSerializer)

val event = NavigationEvent(snapshot = snapshot)

if (event !is NavigationEvent.Forward) {
throw SerializationException("${this::class.simpleName} only works for ${NavigationEvent.Forward::class.simpleName} types.")
}

return event
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is NavigationEventForwardSerializer<*, *>) return false

if (delegateSerializer != other.delegateSerializer) return false

return descriptor == other.descriptor
}

override fun hashCode(): Int {
var result = delegateSerializer.hashCode()
result = 31 * result + descriptor.hashCode()
return result
}

override fun toString(): String =
"NavigationEventForwardSerializer(" +
"delegateSerializer=$delegateSerializer, " +
"descriptor=$descriptor)"
}
Loading