Skip to content

Commit

Permalink
Refactored new navigation approach
Browse files Browse the repository at this point in the history
  • Loading branch information
chRyNaN committed May 22, 2023
1 parent 00e665f commit cb39a6d
Show file tree
Hide file tree
Showing 12 changed files with 784 additions and 548 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package com.chrynan.navigation

import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

Expand All @@ -13,24 +14,31 @@ import kotlinx.serialization.encoding.Encoder
*/
@Serializable(with = NavigationContextStacksSerializer::class)
internal class NavigationContextStacks<Destination : NavigationDestination, Context : NavigationContext<Destination>> internal constructor(
internal val initialContext: Context
internal val initialContext: Context,
contextStacks: Map<Context, Stack<Destination>> = emptyMap()
) {

/**
* A read-only version of the internal context map stack state.
*/
internal val contextStacks: Map<Context, Stack<Destination>>
get() = destinationStacksByContext

private val destinationStacksByContext =
mutableMapOf(initialContext to mutableStackOf(initialContext.initialDestination))
private val destinationStacksByContext: MutableMap<Context, MutableStack<Destination>> =
mutableMapOf(initialContext to mutableStackOf(initialContext.initialDestination)).apply {
contextStacks.entries.forEach { entry ->
this[entry.key] = entry.value.toMutableStack()
}
}

/**
* Retrieves the [Stack] of [Destination]s for the provided [Context].
*/
operator fun get(context: Context): Stack<Destination> =
destinationStacksByContext[context]?.toStack() ?: stackOf(context.initialDestination)

/**
* Sets the [Stack] for the provided [Context]. Note that this does not perform a check to make sure that the
* initial destination is correct.
*/
operator fun set(context: Context, destinations: Stack<Destination>) {
destinationStacksByContext[context] = destinations.toMutableStack()
}

/**
* Retrieves the current [Destination] on top of the [Stack] for the provided [Context] without removing it.
*/
Expand Down Expand Up @@ -65,6 +73,29 @@ internal class NavigationContextStacks<Destination : NavigationDestination, Cont
destinationStacksByContext[context] = stack
}

/**
* Pushes the provided [destination] to the top of the [Stack] of [Destination]s for the provided [context], but
* if the provided [destination] already exists in the [Stack] of [Destination]s for the provided [context], all
* the items on top of it will be popped from the stack.
*/
fun pushDropping(context: Context, destination: Destination) {
val stack = destinationStacksByContext[context] ?: mutableStackOf(context.initialDestination)

val index = stack.indexOf(destination)

if (index == -1) {
stack.push(destination)
} else {
val dropCount = (stack.size - (stack.size - index)) + 1

stack.drop(dropCount)

stack.push(destination)
}

destinationStacksByContext[context] = stack
}

/**
* Pushes all the provided [destinations] to the top of the [Stack] of [Destination]s for the provided [context].
*/
Expand All @@ -76,75 +107,68 @@ internal class NavigationContextStacks<Destination : NavigationDestination, Cont
destinationStacksByContext[context] = stack
}

/**
* Clears the stack for the provided [context] and resets it back to its initial state.
*/
fun clear(context: Context) {
destinationStacksByContext[context] = mutableStackOf(context.initialDestination)
}

/**
* Clears all the [Stack]s of [Destination]s for all the [Context]s. This resets the internal data structure back
* to its initial state.
*/
fun clear() {
fun clearAll() {
destinationStacksByContext.clear()
destinationStacksByContext[initialContext] = mutableStackOf(initialContext.initialDestination)
}

/**
* Returns a [Map] of the [Context] to [Stack] of [Destination]s.
*/
fun toMap(): Map<Context, Stack<Destination>> = destinationStacksByContext
}

/**
* Represents a snapshot of a [NavigationContextStacks] that can be persisted and obtained later to create a
* [NavigationContextStacks] with the same values of this snapshot.
* Pops the top destination off the provided [context] stack and returns the new top destination, or `null` if the
* provided [context] stack could not be popped (there must be at least one item in the stack at all times).
*/
@Serializable
internal class PersistedNavigationContextStacksSnapshot<Destination : NavigationDestination, Context : NavigationContext<Destination>> internal constructor(
@SerialName(value = "initial_context") val initialContext: Context,
@SerialName(value = "context_stacks") val contextStacks: Map<Context, Stack<Destination>>
) {

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

if (initialContext != other.initialContext) return false
internal fun <Destination : NavigationDestination, Context : NavigationContext<Destination>> NavigationContextStacks<Destination, Context>.popToPreviousDestinationForContext(
context: Context
): Destination? {
this.pop(context) ?: return null

return contextStacks == other.contextStacks
}

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

override fun toString(): String =
"PersistedNavigationContextStacksSnapshot(initialContext=$initialContext, contextStacks=$contextStacks)"
return this.peek(context)
}

/**
* A [KSerializer] for [NavigationContextStacks].
*/
internal class NavigationContextStacksSerializer<Destination : NavigationDestination, Context : NavigationContext<Destination>> internal constructor(
destinationSerializer: KSerializer<Destination>,
contextSerializer: KSerializer<Context>
private val contextSerializer: KSerializer<Context>
) : KSerializer<NavigationContextStacks<Destination, Context>> {

private val delegateSerializer =
PersistedNavigationContextStacksSnapshot.serializer(destinationSerializer, contextSerializer)
private val stackSerializer = StackSerializer(destinationSerializer)
private val mapSerializer = MapSerializer(contextSerializer, stackSerializer)

override val descriptor: SerialDescriptor
get() = delegateSerializer.descriptor
override val descriptor: SerialDescriptor = buildClassSerialDescriptor(serialName = "NavigationContextStacks") {
element(elementName = "initialContext", descriptor = contextSerializer.descriptor)
element(elementName = "context_stacks", descriptor = mapSerializer.descriptor)
}

override fun serialize(encoder: Encoder, value: NavigationContextStacks<Destination, Context>) {
val snapshot = PersistedNavigationContextStacksSnapshot(
initialContext = value.initialContext,
contextStacks = value.contextStacks
)

delegateSerializer.serialize(encoder = encoder, value = snapshot)
encoder.encodeSerializableValue(serializer = contextSerializer, value = value.initialContext)
encoder.encodeSerializableValue(serializer = mapSerializer, value = value.toMap())
}

override fun deserialize(decoder: Decoder): NavigationContextStacks<Destination, Context> {
val snapshot = delegateSerializer.deserialize(decoder = decoder)
val initialContext = decoder.decodeSerializableValue(deserializer = contextSerializer)
val contextStacks = decoder.decodeSerializableValue(deserializer = mapSerializer)

return NavigationContextStacks(initialContext = snapshot.initialContext).apply {
snapshot.contextStacks.forEach { (context, destinations) ->
this.pushAll(context = context, destinations = destinations)
}
}
return NavigationContextStacks(
initialContext = initialContext,
contextStacks = contextStacks
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import kotlinx.serialization.Serializable
* change in [NavigationContext], or a [NavigationEvent.Backward] representing a back tracking of a previous
* [NavigationEvent].
*
* @see [Navigator.navigate]
* @see [Navigator.dispatch]
*/
@Serializable
sealed class NavigationEvent<D : NavigationDestination, C : NavigationContext<D>> private constructor() {
Expand Down Expand Up @@ -57,14 +57,11 @@ sealed class NavigationEvent<D : NavigationDestination, C : NavigationContext<D>
* defined by the provided [kind] property.
*
* @property [instant] The [Instant] that the event occurred.
* @property [kind] The [Kind] of supported back navigation (across contexts or just destinations within the
* current context).
*/
@Serializable
@SerialName(value = "back")
class Backward<D : NavigationDestination, C : NavigationContext<D>> internal constructor(
@SerialName(value = "instant") override val instant: Instant = Clock.System.now(),
@SerialName(value = "kind") val kind: Kind
@SerialName(value = "instant") override val instant: Instant = Clock.System.now()
) : NavigationEvent<D, C>() {

override val direction: Direction = Direction.BACKWARDS
Expand All @@ -73,47 +70,14 @@ sealed class NavigationEvent<D : NavigationDestination, C : NavigationContext<D>
if (this === other) return true
if (other !is Backward<*, *>) return false

if (instant != other.instant) return false

return kind == other.kind
return instant == other.instant
}

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

override fun toString(): String =
"NavigationEvent.Backward(instant=$instant, type=$kind, direction=$direction)"

/**
* Represents the type of supported back navigation. An [IN_CONTEXT] value indicates that navigation to the
* previous [NavigationDestination] in the current [NavigationContext] should occur. An [ACROSS_CONTEXTS] value
* indicates that navigation across [NavigationContext]s is allowed, meaning that navigation can either be to
* the previous [NavigationDestination] within the current [NavigationContext] or to the previous
* [NavigationContext] depending on whether the previous [NavigationEvent] was a
* [NavigationEvent.Forward.Destination] or [NavigationEvent.Forward.Context] event.
*/
@Serializable
enum class Kind(val serialName: String) {

/**
* Indicates that navigation to the previous [NavigationDestination] in the current [NavigationContext]
* should occur
*/
@SerialName(value = "in_context")
IN_CONTEXT(serialName = "in_context"),

/**
* Indicates that navigation across [NavigationContext]s is allowed, meaning that navigation can either be
* to the previous [NavigationDestination] within the current [NavigationContext] or to the previous
* [NavigationContext] depending on whether the previous [NavigationEvent] was a
* [NavigationEvent.Forward.Destination] or [NavigationEvent.Forward.Context] event
*/
@SerialName(value = "across_context")
ACROSS_CONTEXTS(serialName = "across_context")
}
"NavigationEvent.Backward(instant=$instant, direction=$direction)"
}

/**
Expand Down
Loading

0 comments on commit cb39a6d

Please sign in to comment.