Skip to content

Commit

Permalink
feat: voyager state restoration and web history mode
Browse files Browse the repository at this point in the history
  • Loading branch information
programadorthi committed Jun 9, 2024
1 parent 1d345c3 commit ca88590
Show file tree
Hide file tree
Showing 18 changed files with 515 additions and 20 deletions.
9 changes: 9 additions & 0 deletions integration/voyager/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ kotlin {
}
}

val jsMain by getting {
dependencies {
implementation(libs.serialization.json)
}
}

val jvmMain by getting {
dependsOn(commonMain.get())
dependencies {
Expand Down Expand Up @@ -74,5 +80,8 @@ kotlin {
val iosArm64Main by getting {
dependsOn(nativeMain)
}
val iosSimulatorArm64Main by getting {
dependsOn(nativeMain)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package dev.programadorthi.routing.voyager

import cafe.adriel.voyager.navigator.Navigator
import dev.programadorthi.routing.core.Routing
import dev.programadorthi.routing.core.application
import dev.programadorthi.routing.core.application.Application
import dev.programadorthi.routing.core.application.ApplicationCall
import io.ktor.util.AttributeKey
Expand All @@ -26,3 +28,9 @@ internal var ApplicationCall.voyagerNavigator: Navigator
set(value) {
application.voyagerNavigator = value
}

internal var Routing.voyagerNavigator: Navigator
get() = application.voyagerNavigator
set(value) {
application.voyagerNavigator = value
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import dev.programadorthi.routing.core.Route
import dev.programadorthi.routing.core.RouteMethod
import dev.programadorthi.routing.core.Routing
import dev.programadorthi.routing.core.application.ApplicationCall
import dev.programadorthi.routing.core.asRouting
import dev.programadorthi.routing.resources.handle
import dev.programadorthi.routing.resources.unregisterResource
import io.ktor.util.pipeline.PipelineContext
Expand All @@ -16,15 +17,17 @@ import io.ktor.util.pipeline.PipelineContext
*
* @param body receives an instance of the typed resource [T] as the first parameter.
*/
public inline fun <reified T : Any> Route.screen(noinline body: suspend PipelineContext<Unit, ApplicationCall>.(T) -> Screen): Route =
handle<T> { resource ->
screen {
public inline fun <reified T : Any> Route.screen(noinline body: suspend PipelineContext<Unit, ApplicationCall>.(T) -> Screen): Route {
val routing = asRouting ?: error("Your route $this must have a parent Routing")
return handle<T> { resource ->
screen(routing) {
when (resource) {
is Screen -> resource
else -> body(resource)
}
}
}
}

/**
* Registers a typed handler for a [Screen] defined by the [T] class.
Expand All @@ -43,15 +46,17 @@ public inline fun <reified T : Screen> Route.screen(): Route = screen<T> { scree
public inline fun <reified T : Any> Route.screen(
method: RouteMethod,
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(T) -> Screen,
): Route =
handle<T>(method = method) { resource ->
screen {
): Route {
val routing = asRouting ?: error("Your route $this must have a parent Routing")
return handle<T>(method = method) { resource ->
screen(routing) {
when (resource) {
is Screen -> resource
else -> body(resource)
}
}
}
}

/**
* Registers a typed handler for a [RouteMethod] [Screen] defined by the [T] class.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.currentCompositeKeyHash
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.CurrentScreen
Expand All @@ -16,8 +19,12 @@ import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior
import cafe.adriel.voyager.navigator.OnBackPressed
import dev.programadorthi.routing.core.Route
import dev.programadorthi.routing.core.Routing
import dev.programadorthi.routing.core.application
import dev.programadorthi.routing.core.application.ApplicationCall
import dev.programadorthi.routing.core.replace
import dev.programadorthi.routing.core.routing
import dev.programadorthi.routing.voyager.history.VoyagerHistoryMode
import dev.programadorthi.routing.voyager.history.historyMode
import dev.programadorthi.routing.voyager.history.restoreState
import io.ktor.util.logging.Logger
import kotlin.coroutines.CoroutineContext

Expand All @@ -28,6 +35,7 @@ public val LocalVoyagerRouting: ProvidableCompositionLocal<Routing> =

@Composable
public fun VoyagerRouting(
historyMode: VoyagerHistoryMode = VoyagerHistoryMode.Memory,
routing: Routing,
initialScreen: Screen,
disposeBehavior: NavigatorDisposeBehavior = NavigatorDisposeBehavior(),
Expand All @@ -36,14 +44,31 @@ public fun VoyagerRouting(
content: NavigatorContent = { CurrentScreen() },
) {
CompositionLocalProvider(LocalVoyagerRouting provides routing) {
var stateToRestore by remember { mutableStateOf<Any?>(null) }

routing.restoreState { state ->
stateToRestore = state
}

Navigator(
screen = initialScreen,
disposeBehavior = disposeBehavior,
onBackPressed = onBackPressed,
key = key,
) { navigator ->
SideEffect {
routing.application.voyagerNavigator = navigator
routing.voyagerNavigator = navigator
routing.historyMode = historyMode

if (stateToRestore != null) {
val call = stateToRestore as? ApplicationCall
val path = stateToRestore as? String ?: ""
when {
call != null -> routing.execute(call)
path.isNotBlank() -> routing.replace(path)
}
stateToRestore = null
}
}
content(navigator)
}
Expand All @@ -52,6 +77,7 @@ public fun VoyagerRouting(

@Composable
public fun VoyagerRouting(
historyMode: VoyagerHistoryMode = VoyagerHistoryMode.Memory,
initialScreen: Screen,
configuration: Route.() -> Unit,
rootPath: String = "/",
Expand Down Expand Up @@ -83,6 +109,7 @@ public fun VoyagerRouting(
}

VoyagerRouting(
historyMode = historyMode,
routing = routing,
initialScreen = initialScreen,
disposeBehavior = disposeBehavior,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ package dev.programadorthi.routing.voyager
import cafe.adriel.voyager.core.screen.Screen
import dev.programadorthi.routing.core.Route
import dev.programadorthi.routing.core.RouteMethod
import dev.programadorthi.routing.core.Routing
import dev.programadorthi.routing.core.application.ApplicationCall
import dev.programadorthi.routing.core.application.call
import dev.programadorthi.routing.core.asRouting
import dev.programadorthi.routing.core.route
import dev.programadorthi.routing.voyager.history.platformPush
import dev.programadorthi.routing.voyager.history.platformReplace
import dev.programadorthi.routing.voyager.history.platformReplaceAll
import dev.programadorthi.routing.voyager.history.shouldNeglect
import io.ktor.util.pipeline.PipelineContext
import io.ktor.utils.io.KtorDsl

Expand All @@ -26,19 +32,27 @@ public fun Route.screen(

@KtorDsl
public fun Route.screen(body: suspend PipelineContext<Unit, ApplicationCall>.() -> Screen) {
val routing = asRouting ?: error("Your route $this must have a parent Routing")
handle {
screen {
screen(routing) {
body(this)
}
}
}

public suspend fun PipelineContext<Unit, ApplicationCall>.screen(body: suspend () -> Screen) {
val navigator = call.voyagerNavigator
public suspend fun PipelineContext<Unit, ApplicationCall>.screen(
routing: Routing,
body: suspend () -> Screen,
) {
if (call.shouldNeglect()) {
call.voyagerNavigator.replace(body())
return
}

when (call.routeMethod) {
RouteMethod.Push -> navigator.push(body())
RouteMethod.Replace -> navigator.replace(body())
RouteMethod.ReplaceAll -> navigator.replaceAll(body())
RouteMethod.Push -> call.platformPush(routing, body)
RouteMethod.Replace -> call.platformReplace(routing, body)
RouteMethod.ReplaceAll -> call.platformReplaceAll(routing, body)
else ->
error(
"Voyager needs a stack route method to work. You called a screen ${call.uri} using " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,30 @@ package dev.programadorthi.routing.voyager
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.Navigator
import dev.programadorthi.routing.core.Routing
import dev.programadorthi.routing.core.application

public fun Routing.canPop(): Boolean = application.voyagerNavigator.canPop
internal expect fun Routing.popOnPlatform(
result: Any? = null,
fallback: () -> Unit,
)

public expect val Routing.canPop: Boolean

public fun Routing.canPop(): Boolean = canPop

public fun Routing.pop(result: Any? = null) {
val navigator = application.voyagerNavigator
if (navigator.pop()) {
navigator.trySendPopResult(result)
popOnPlatform(result) {
val navigator = voyagerNavigator
if (navigator.pop()) {
navigator.trySendPopResult(result)
}
}
}

public fun Routing.popUntil(
result: Any? = null,
predicate: (Screen) -> Boolean,
) {
val navigator = application.voyagerNavigator
val navigator = voyagerNavigator
if (navigator.popUntil(predicate)) {
navigator.trySendPopResult(result)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package dev.programadorthi.routing.voyager.history

import dev.programadorthi.routing.core.Routing
import dev.programadorthi.routing.core.application
import dev.programadorthi.routing.core.application.Application
import io.ktor.util.AttributeKey

internal val VoyagerHistoryModeAttributeKey: AttributeKey<VoyagerHistoryMode> =
AttributeKey("VoyagerHistoryModeAttributeKey")

internal var Application.historyMode: VoyagerHistoryMode
get() = attributes[VoyagerHistoryModeAttributeKey]
set(value) {
attributes.put(VoyagerHistoryModeAttributeKey, value)
}

internal var Routing.historyMode: VoyagerHistoryMode
get() = application.historyMode
set(value) {
application.historyMode = value
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package dev.programadorthi.routing.voyager.history

import androidx.compose.runtime.Composable
import cafe.adriel.voyager.core.screen.Screen
import dev.programadorthi.routing.core.Routing
import dev.programadorthi.routing.core.application.ApplicationCall

internal expect suspend fun ApplicationCall.platformPush(
routing: Routing,
body: suspend () -> Screen,
)

internal expect suspend fun ApplicationCall.platformReplace(
routing: Routing,
body: suspend () -> Screen,
)

internal expect suspend fun ApplicationCall.platformReplaceAll(
routing: Routing,
body: suspend () -> Screen,
)

internal expect fun ApplicationCall.shouldNeglect(): Boolean

@Composable
internal expect fun Routing.restoreState(onState: (Any) -> Unit)
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package dev.programadorthi.routing.voyager.history

/**
* Options that how the web history is controlled
*
* These options affects web application only. Memory will be used by default in other targets
*/
public enum class VoyagerHistoryMode {
/**
* Hash URLs pattern. E.g: host/#/path
* Each route will have an entry on the browser history.
* To avoid browser history, set neglect = true before routing to a route
*/
Hash,

/**
* Traditional URLs pattern. E.g: host/path
* Each route will have an entry on the browser history.
* To avoid browser history, set neglect = true before routing to a route
*/
Html5,

/**
* No updates to URL or History stack.
* All route will be neglected.
*/
Memory,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package dev.programadorthi.routing.voyager.history

import dev.programadorthi.routing.core.RouteMethod
import dev.programadorthi.routing.core.application.Application
import dev.programadorthi.routing.core.application.ApplicationCall
import io.ktor.http.parametersOf
import io.ktor.util.toMap
import kotlinx.serialization.Serializable

@Serializable
internal data class VoyagerHistoryState(
val routeMethod: String,
val name: String,
val uri: String,
val parameters: Map<String, List<String>>,
)

internal fun VoyagerHistoryState.toCall(application: Application): ApplicationCall {
return ApplicationCall(
application = application,
name = name,
uri = uri,
routeMethod = RouteMethod.parse(routeMethod),
parameters = parametersOf(parameters),
)
}

internal fun ApplicationCall.toHistoryState(): VoyagerHistoryState {
return VoyagerHistoryState(
routeMethod = routeMethod.value,
name = name,
uri = uri,
parameters = parameters.toMap(),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.programadorthi.routing.voyager

import dev.programadorthi.routing.core.Routing
import dev.programadorthi.routing.voyager.history.VoyagerHistoryMode
import dev.programadorthi.routing.voyager.history.historyMode
import dev.programadorthi.routing.voyager.history.popWindowHistory

internal actual fun Routing.popOnPlatform(
result: Any?,
fallback: () -> Unit,
) {
when (historyMode) {
VoyagerHistoryMode.Memory -> fallback()
else -> popWindowHistory()
}
}

public actual val Routing.canPop: Boolean
get() = historyMode != VoyagerHistoryMode.Memory || voyagerNavigator.canPop
Loading

0 comments on commit ca88590

Please sign in to comment.