Skip to content

Commit ca88590

Browse files
feat: voyager state restoration and web history mode
1 parent 1d345c3 commit ca88590

File tree

18 files changed

+515
-20
lines changed

18 files changed

+515
-20
lines changed

integration/voyager/build.gradle.kts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ kotlin {
4040
}
4141
}
4242

43+
val jsMain by getting {
44+
dependencies {
45+
implementation(libs.serialization.json)
46+
}
47+
}
48+
4349
val jvmMain by getting {
4450
dependsOn(commonMain.get())
4551
dependencies {
@@ -74,5 +80,8 @@ kotlin {
7480
val iosArm64Main by getting {
7581
dependsOn(nativeMain)
7682
}
83+
val iosSimulatorArm64Main by getting {
84+
dependsOn(nativeMain)
85+
}
7786
}
7887
}

integration/voyager/common/src/dev/programadorthi/routing/voyager/VoyagerNavigatorAttribute.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package dev.programadorthi.routing.voyager
22

33
import cafe.adriel.voyager.navigator.Navigator
4+
import dev.programadorthi.routing.core.Routing
5+
import dev.programadorthi.routing.core.application
46
import dev.programadorthi.routing.core.application.Application
57
import dev.programadorthi.routing.core.application.ApplicationCall
68
import io.ktor.util.AttributeKey
@@ -26,3 +28,9 @@ internal var ApplicationCall.voyagerNavigator: Navigator
2628
set(value) {
2729
application.voyagerNavigator = value
2830
}
31+
32+
internal var Routing.voyagerNavigator: Navigator
33+
get() = application.voyagerNavigator
34+
set(value) {
35+
application.voyagerNavigator = value
36+
}

integration/voyager/common/src/dev/programadorthi/routing/voyager/VoyagerResourcesBuilder.kt

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import dev.programadorthi.routing.core.Route
55
import dev.programadorthi.routing.core.RouteMethod
66
import dev.programadorthi.routing.core.Routing
77
import dev.programadorthi.routing.core.application.ApplicationCall
8+
import dev.programadorthi.routing.core.asRouting
89
import dev.programadorthi.routing.resources.handle
910
import dev.programadorthi.routing.resources.unregisterResource
1011
import io.ktor.util.pipeline.PipelineContext
@@ -16,15 +17,17 @@ import io.ktor.util.pipeline.PipelineContext
1617
*
1718
* @param body receives an instance of the typed resource [T] as the first parameter.
1819
*/
19-
public inline fun <reified T : Any> Route.screen(noinline body: suspend PipelineContext<Unit, ApplicationCall>.(T) -> Screen): Route =
20-
handle<T> { resource ->
21-
screen {
20+
public inline fun <reified T : Any> Route.screen(noinline body: suspend PipelineContext<Unit, ApplicationCall>.(T) -> Screen): Route {
21+
val routing = asRouting ?: error("Your route $this must have a parent Routing")
22+
return handle<T> { resource ->
23+
screen(routing) {
2224
when (resource) {
2325
is Screen -> resource
2426
else -> body(resource)
2527
}
2628
}
2729
}
30+
}
2831

2932
/**
3033
* Registers a typed handler for a [Screen] defined by the [T] class.
@@ -43,15 +46,17 @@ public inline fun <reified T : Screen> Route.screen(): Route = screen<T> { scree
4346
public inline fun <reified T : Any> Route.screen(
4447
method: RouteMethod,
4548
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(T) -> Screen,
46-
): Route =
47-
handle<T>(method = method) { resource ->
48-
screen {
49+
): Route {
50+
val routing = asRouting ?: error("Your route $this must have a parent Routing")
51+
return handle<T>(method = method) { resource ->
52+
screen(routing) {
4953
when (resource) {
5054
is Screen -> resource
5155
else -> body(resource)
5256
}
5357
}
5458
}
59+
}
5560

5661
/**
5762
* Registers a typed handler for a [RouteMethod] [Screen] defined by the [T] class.

integration/voyager/common/src/dev/programadorthi/routing/voyager/VoyagerRouting.kt

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import androidx.compose.runtime.DisposableEffect
66
import androidx.compose.runtime.ProvidableCompositionLocal
77
import androidx.compose.runtime.SideEffect
88
import androidx.compose.runtime.currentCompositeKeyHash
9+
import androidx.compose.runtime.getValue
10+
import androidx.compose.runtime.mutableStateOf
911
import androidx.compose.runtime.remember
12+
import androidx.compose.runtime.setValue
1013
import androidx.compose.runtime.staticCompositionLocalOf
1114
import cafe.adriel.voyager.core.screen.Screen
1215
import cafe.adriel.voyager.navigator.CurrentScreen
@@ -16,8 +19,12 @@ import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior
1619
import cafe.adriel.voyager.navigator.OnBackPressed
1720
import dev.programadorthi.routing.core.Route
1821
import dev.programadorthi.routing.core.Routing
19-
import dev.programadorthi.routing.core.application
22+
import dev.programadorthi.routing.core.application.ApplicationCall
23+
import dev.programadorthi.routing.core.replace
2024
import dev.programadorthi.routing.core.routing
25+
import dev.programadorthi.routing.voyager.history.VoyagerHistoryMode
26+
import dev.programadorthi.routing.voyager.history.historyMode
27+
import dev.programadorthi.routing.voyager.history.restoreState
2128
import io.ktor.util.logging.Logger
2229
import kotlin.coroutines.CoroutineContext
2330

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

2936
@Composable
3037
public fun VoyagerRouting(
38+
historyMode: VoyagerHistoryMode = VoyagerHistoryMode.Memory,
3139
routing: Routing,
3240
initialScreen: Screen,
3341
disposeBehavior: NavigatorDisposeBehavior = NavigatorDisposeBehavior(),
@@ -36,14 +44,31 @@ public fun VoyagerRouting(
3644
content: NavigatorContent = { CurrentScreen() },
3745
) {
3846
CompositionLocalProvider(LocalVoyagerRouting provides routing) {
47+
var stateToRestore by remember { mutableStateOf<Any?>(null) }
48+
49+
routing.restoreState { state ->
50+
stateToRestore = state
51+
}
52+
3953
Navigator(
4054
screen = initialScreen,
4155
disposeBehavior = disposeBehavior,
4256
onBackPressed = onBackPressed,
4357
key = key,
4458
) { navigator ->
4559
SideEffect {
46-
routing.application.voyagerNavigator = navigator
60+
routing.voyagerNavigator = navigator
61+
routing.historyMode = historyMode
62+
63+
if (stateToRestore != null) {
64+
val call = stateToRestore as? ApplicationCall
65+
val path = stateToRestore as? String ?: ""
66+
when {
67+
call != null -> routing.execute(call)
68+
path.isNotBlank() -> routing.replace(path)
69+
}
70+
stateToRestore = null
71+
}
4772
}
4873
content(navigator)
4974
}
@@ -52,6 +77,7 @@ public fun VoyagerRouting(
5277

5378
@Composable
5479
public fun VoyagerRouting(
80+
historyMode: VoyagerHistoryMode = VoyagerHistoryMode.Memory,
5581
initialScreen: Screen,
5682
configuration: Route.() -> Unit,
5783
rootPath: String = "/",
@@ -83,6 +109,7 @@ public fun VoyagerRouting(
83109
}
84110

85111
VoyagerRouting(
112+
historyMode = historyMode,
86113
routing = routing,
87114
initialScreen = initialScreen,
88115
disposeBehavior = disposeBehavior,

integration/voyager/common/src/dev/programadorthi/routing/voyager/VoyagerRoutingBuilder.kt

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@ package dev.programadorthi.routing.voyager
33
import cafe.adriel.voyager.core.screen.Screen
44
import dev.programadorthi.routing.core.Route
55
import dev.programadorthi.routing.core.RouteMethod
6+
import dev.programadorthi.routing.core.Routing
67
import dev.programadorthi.routing.core.application.ApplicationCall
78
import dev.programadorthi.routing.core.application.call
9+
import dev.programadorthi.routing.core.asRouting
810
import dev.programadorthi.routing.core.route
11+
import dev.programadorthi.routing.voyager.history.platformPush
12+
import dev.programadorthi.routing.voyager.history.platformReplace
13+
import dev.programadorthi.routing.voyager.history.platformReplaceAll
14+
import dev.programadorthi.routing.voyager.history.shouldNeglect
915
import io.ktor.util.pipeline.PipelineContext
1016
import io.ktor.utils.io.KtorDsl
1117

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

2733
@KtorDsl
2834
public fun Route.screen(body: suspend PipelineContext<Unit, ApplicationCall>.() -> Screen) {
35+
val routing = asRouting ?: error("Your route $this must have a parent Routing")
2936
handle {
30-
screen {
37+
screen(routing) {
3138
body(this)
3239
}
3340
}
3441
}
3542

36-
public suspend fun PipelineContext<Unit, ApplicationCall>.screen(body: suspend () -> Screen) {
37-
val navigator = call.voyagerNavigator
43+
public suspend fun PipelineContext<Unit, ApplicationCall>.screen(
44+
routing: Routing,
45+
body: suspend () -> Screen,
46+
) {
47+
if (call.shouldNeglect()) {
48+
call.voyagerNavigator.replace(body())
49+
return
50+
}
51+
3852
when (call.routeMethod) {
39-
RouteMethod.Push -> navigator.push(body())
40-
RouteMethod.Replace -> navigator.replace(body())
41-
RouteMethod.ReplaceAll -> navigator.replaceAll(body())
53+
RouteMethod.Push -> call.platformPush(routing, body)
54+
RouteMethod.Replace -> call.platformReplace(routing, body)
55+
RouteMethod.ReplaceAll -> call.platformReplaceAll(routing, body)
4256
else ->
4357
error(
4458
"Voyager needs a stack route method to work. You called a screen ${call.uri} using " +

integration/voyager/common/src/dev/programadorthi/routing/voyager/VoyagerRoutingExt.kt

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,30 @@ package dev.programadorthi.routing.voyager
33
import cafe.adriel.voyager.core.screen.Screen
44
import cafe.adriel.voyager.navigator.Navigator
55
import dev.programadorthi.routing.core.Routing
6-
import dev.programadorthi.routing.core.application
76

8-
public fun Routing.canPop(): Boolean = application.voyagerNavigator.canPop
7+
internal expect fun Routing.popOnPlatform(
8+
result: Any? = null,
9+
fallback: () -> Unit,
10+
)
11+
12+
public expect val Routing.canPop: Boolean
13+
14+
public fun Routing.canPop(): Boolean = canPop
915

1016
public fun Routing.pop(result: Any? = null) {
11-
val navigator = application.voyagerNavigator
12-
if (navigator.pop()) {
13-
navigator.trySendPopResult(result)
17+
popOnPlatform(result) {
18+
val navigator = voyagerNavigator
19+
if (navigator.pop()) {
20+
navigator.trySendPopResult(result)
21+
}
1422
}
1523
}
1624

1725
public fun Routing.popUntil(
1826
result: Any? = null,
1927
predicate: (Screen) -> Boolean,
2028
) {
21-
val navigator = application.voyagerNavigator
29+
val navigator = voyagerNavigator
2230
if (navigator.popUntil(predicate)) {
2331
navigator.trySendPopResult(result)
2432
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package dev.programadorthi.routing.voyager.history
2+
3+
import dev.programadorthi.routing.core.Routing
4+
import dev.programadorthi.routing.core.application
5+
import dev.programadorthi.routing.core.application.Application
6+
import io.ktor.util.AttributeKey
7+
8+
internal val VoyagerHistoryModeAttributeKey: AttributeKey<VoyagerHistoryMode> =
9+
AttributeKey("VoyagerHistoryModeAttributeKey")
10+
11+
internal var Application.historyMode: VoyagerHistoryMode
12+
get() = attributes[VoyagerHistoryModeAttributeKey]
13+
set(value) {
14+
attributes.put(VoyagerHistoryModeAttributeKey, value)
15+
}
16+
17+
internal var Routing.historyMode: VoyagerHistoryMode
18+
get() = application.historyMode
19+
set(value) {
20+
application.historyMode = value
21+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package dev.programadorthi.routing.voyager.history
2+
3+
import androidx.compose.runtime.Composable
4+
import cafe.adriel.voyager.core.screen.Screen
5+
import dev.programadorthi.routing.core.Routing
6+
import dev.programadorthi.routing.core.application.ApplicationCall
7+
8+
internal expect suspend fun ApplicationCall.platformPush(
9+
routing: Routing,
10+
body: suspend () -> Screen,
11+
)
12+
13+
internal expect suspend fun ApplicationCall.platformReplace(
14+
routing: Routing,
15+
body: suspend () -> Screen,
16+
)
17+
18+
internal expect suspend fun ApplicationCall.platformReplaceAll(
19+
routing: Routing,
20+
body: suspend () -> Screen,
21+
)
22+
23+
internal expect fun ApplicationCall.shouldNeglect(): Boolean
24+
25+
@Composable
26+
internal expect fun Routing.restoreState(onState: (Any) -> Unit)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package dev.programadorthi.routing.voyager.history
2+
3+
/**
4+
* Options that how the web history is controlled
5+
*
6+
* These options affects web application only. Memory will be used by default in other targets
7+
*/
8+
public enum class VoyagerHistoryMode {
9+
/**
10+
* Hash URLs pattern. E.g: host/#/path
11+
* Each route will have an entry on the browser history.
12+
* To avoid browser history, set neglect = true before routing to a route
13+
*/
14+
Hash,
15+
16+
/**
17+
* Traditional URLs pattern. E.g: host/path
18+
* Each route will have an entry on the browser history.
19+
* To avoid browser history, set neglect = true before routing to a route
20+
*/
21+
Html5,
22+
23+
/**
24+
* No updates to URL or History stack.
25+
* All route will be neglected.
26+
*/
27+
Memory,
28+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package dev.programadorthi.routing.voyager.history
2+
3+
import dev.programadorthi.routing.core.RouteMethod
4+
import dev.programadorthi.routing.core.application.Application
5+
import dev.programadorthi.routing.core.application.ApplicationCall
6+
import io.ktor.http.parametersOf
7+
import io.ktor.util.toMap
8+
import kotlinx.serialization.Serializable
9+
10+
@Serializable
11+
internal data class VoyagerHistoryState(
12+
val routeMethod: String,
13+
val name: String,
14+
val uri: String,
15+
val parameters: Map<String, List<String>>,
16+
)
17+
18+
internal fun VoyagerHistoryState.toCall(application: Application): ApplicationCall {
19+
return ApplicationCall(
20+
application = application,
21+
name = name,
22+
uri = uri,
23+
routeMethod = RouteMethod.parse(routeMethod),
24+
parameters = parametersOf(parameters),
25+
)
26+
}
27+
28+
internal fun ApplicationCall.toHistoryState(): VoyagerHistoryState {
29+
return VoyagerHistoryState(
30+
routeMethod = routeMethod.value,
31+
name = name,
32+
uri = uri,
33+
parameters = parameters.toMap(),
34+
)
35+
}

0 commit comments

Comments
 (0)