Skip to content

Commit

Permalink
Add configureBrowserNavigation method for web targets to show a cur…
Browse files Browse the repository at this point in the history
…rent route in a browser and enable browser navigation calls.
  • Loading branch information
terrakok committed Oct 7, 2024
1 parent 87bbb6f commit 0424c64
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 0 deletions.
10 changes: 10 additions & 0 deletions navigation/navigation-runtime/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ kotlin {
dependsOn(jbMain)
}

webMain.dependsOn(jbMain)

targets.all { target ->
if (target.platformType !in [
KotlinPlatformType.androidJvm,
Expand All @@ -147,6 +149,14 @@ kotlin {
}
}
}

targets.all { target ->
if (target.platformType == KotlinPlatformType.native) {
target.compilations["main"].defaultSourceSet.dependsOn(jbMain)
} else if (target.platformType in [KotlinPlatformType.js, KotlinPlatformType.wasm]) {
target.compilations["main"].defaultSourceSet.dependsOn(webMain)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.navigation

import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.w3c.dom.PopStateEvent
import org.w3c.dom.Window
import org.w3c.dom.events.Event

actual typealias BrowserWindow = Window

actual fun configureBrowserNavigation(window: Window, navController: NavController) {
var initState = true
var updateState = true

window.addEventListener("popstate", { event: Event ->
if (event is PopStateEvent) { //back or forward in the browser
val state = event.state.toString()

val restoredRoutes = state.lines()
val currentBackStack = navController.currentBackStack.value

//don't handle next navigation calls
updateState = false

//clear current stack
currentBackStack.firstOrNull { it.destination !is NavGraph }?.let { root ->
root.destination.route?.let { navController.popBackStack(it, true) }
}
//restore stack
restoredRoutes.forEach { route -> navController.navigate(route) }
}
})

//global listener is fine here
GlobalScope.launch {
navController.currentBackStack.collect { stack ->
val routes = stack.filter { it.destination !is NavGraph }
.map { it.getRouteWithArgs() ?: return@collect }

val newUri = window.location.run { "$protocol//$host/${routes.last()}" }
val state = routes.joinToString("\n")

if (updateState) {
if (initState) {
window.history.replaceState(state, "", newUri)
initState = false
} else {
window.history.pushState(state, "", newUri)
}
}
updateState = true
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.navigation

import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.w3c.dom.PopStateEvent
import org.w3c.dom.Window
import org.w3c.dom.events.Event

actual typealias BrowserWindow = Window

actual fun configureBrowserNavigation(window: Window, navController: NavController) {
var initState = true
var updateState = true

window.addEventListener("popstate", { event: Event ->
if (event is PopStateEvent) { //back or forward in the browser
val state = event.state?.unsafeCast<JsString>().toString()

val restoredRoutes = state.lines()
val currentBackStack = navController.currentBackStack.value

//don't handle next navigation calls
updateState = false

//clear current stack
currentBackStack.firstOrNull { it.destination !is NavGraph }?.let { root ->
root.destination.route?.let { navController.popBackStack(it, true) }
}
//restore stack
restoredRoutes.forEach { route -> navController.navigate(route) }
}
})

//global listener is fine here
GlobalScope.launch {
navController.currentBackStack.collect { stack ->
val routes = stack.filter { it.destination !is NavGraph }
.map { it.getRouteWithArgs() ?: return@collect }

val newUri = window.location.run { "$protocol//$host/${routes.last()}" }
val state = routes.joinToString("\n")

if (updateState) {
if (initState) {
window.history.replaceState(state.toJsString(), "", newUri)
initState = false
} else {
window.history.pushState(state.toJsString(), "", newUri)
}
}
updateState = true
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.navigation

import androidx.core.bundle.Bundle
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import androidx.navigation.NavGraph

expect abstract class BrowserWindow

/**
* Configures the browser navigation for the given window and navigation controller.
*
* @param window an instance of browser's window to be configured
* @param navController an instance of NavController handling the navigation logic
*/
expect fun configureBrowserNavigation(window: BrowserWindow, navController: NavController)

private val argPlaceholder = Regex("""\{*.\}""")
internal fun NavBackStackEntry.getRouteWithArgs(): String? {
val entry = this
val route = entry.destination.route ?: return null
if (!route.contains(argPlaceholder)) return route
val args = entry.arguments ?: Bundle()
val nameToValue = entry.destination.arguments.map { (name, arg) ->
val serializedTypeValue = arg.type.serializeAsValue(arg.type[args, name])
name to serializedTypeValue
}

val routeWithFilledArgs =
nameToValue.fold(initial = route) { acc, (argumentName: String, value: String) ->
acc.replace("{$argumentName}", value)
}
return routeWithFilledArgs.takeIf { !it.contains(argPlaceholder) }
}

0 comments on commit 0424c64

Please sign in to comment.