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

Browser navigation integration #1621

Merged
merged 11 commits into from
Oct 9, 2024
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
MatkovIvan marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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)
MatkovIvan marked this conversation as resolved.
Show resolved Hide resolved

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)
}
terrakok marked this conversation as resolved.
Show resolved Hide resolved
return routeWithFilledArgs.takeIf { !it.contains(argPlaceholder) }
}