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

Interoperability and wasmJs bugfixes #412

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
package app.softwork.routingcompose

internal class DelegateRouter(val basePath: String, val router: Router) : Router by router {
override fun navigate(to: String, hide: Boolean) {
override fun navigate(to: String, hide: Boolean, replace: Boolean) {
when {
to.startsWith("/") -> {
router.navigate(to, hide)
router.navigate(to, hide, replace)
}

basePath == "/" -> {
router.navigate("/$to", hide)
router.navigate("/$to", hide, replace)
}

to.startsWith(".") -> {
val newPath = router.currentPath.relative(to)
router.navigate(newPath.path)
router.navigate(newPath.path, replace = replace)
}

else -> {
router.navigate("$basePath/$to", hide)
router.navigate("$basePath/$to", hide, replace)
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/commonMain/kotlin/app/softwork/routingcompose/Path.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ public data class Path(val path: String, val parameters: Parameters?) {
)
}

internal companion object {
fun from(rawPath: String): Path {
public companion object {
public fun from(rawPath: String): Path {
val pathAndQuery = rawPath.split("?")
val (path, rawParameters) = when (pathAndQuery.size) {
1 -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import kotlin.uuid.*
*/
@Routing
public class RouteBuilder internal constructor(private val basePath: String, private val remainingPath: Path) {
public val path: String = remainingPath.path
public val parameters: Parameters? = remainingPath.parameters

private var match by mutableStateOf(Match.NoMatch)
Expand Down Expand Up @@ -87,7 +88,9 @@ public class RouteBuilder internal constructor(private val basePath: String, pri
val currentRouter = Router.current
val delegatingRouter = remember(newPath) { DelegateRouter(basePath, currentRouter) }
CompositionLocalProvider(RouterCompositionLocal provides delegatingRouter) {
val newState = RouteBuilder(basePath, newPath)
val newState = routeBuilderCache.getOrPut("${currentRouter.hashCode()} $basePath $newPath") {
RouteBuilder(basePath, newPath)
}
newState.nestedRoute()
}
}
Expand Down Expand Up @@ -166,4 +169,12 @@ public class RouteBuilder internal constructor(private val basePath: String, pri
}
}
}

public companion object {
/**
* Cache for [RouteBuilder] to prevent memory leaks and
* performance degradation after large number of recompositions.
*/
internal var routeBuilderCache = mutableMapOf<String, RouteBuilder>()
}
}
48 changes: 40 additions & 8 deletions src/commonMain/kotlin/app/softwork/routingcompose/Router.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ public interface Router {
*/
public val currentPath: Path

public fun navigate(to: String, hide: Boolean = false)
/**
* Navigate to a new path.
* @param to The path to navigate to.
* @param hide Whether to hide the current path.
* @param replace Whether to replace the current path.
*/
public fun navigate(to: String, hide: Boolean = false, replace: Boolean = false)

@Composable
public fun getPath(initPath: String): State<String>
Expand All @@ -27,6 +33,18 @@ public interface Router {
public val current: Router
@Composable
get() = RouterCompositionLocal.current

/**
* Internal global router instance for use outside of composition.
* Do not use directly, use [global] instead.
*/
public var internalGlobalRouter: Router? = null

/**
* Provide the global router instance for use outside of composition.
*/
public val global: Router
get() = internalGlobalRouter ?: error("Router not defined.")
}
}

Expand All @@ -41,20 +59,34 @@ public fun Router.route(
CompositionLocalProvider(RouterCompositionLocal provides this) {
val rawPath by getPath(initRoute)
val path = Path.from(rawPath)
val node = remember(path) { RouteBuilder(path.path, path) }
val node = remember(path) {
RouteBuilder.routeBuilderCache.getOrPut("${this.hashCode()} ${path.path} $path") {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't we put all parameters to remember? Remember uses its own cache.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remember only works with a recomposition. The routing changes the composition (route calls are leaving and entering the composition when navigating) and remember doesn't work here.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have any samples causing the memory leaks (or a reproducer)? I do understand the cause, we create a new RouteBuilder during each composition, but ideally the GC/Compose should be able to remove the object.

RouteBuilder(path.path, path)
}
}
node.routing()
}
}

public fun Router.navigate(to: String, parameters: Parameters, hide: Boolean = false) {
navigate("$to?$parameters", hide = hide)
public fun Router.navigate(to: String, parameters: Parameters, hide: Boolean = false, replace: Boolean = false) {
navigate("$to?$parameters", hide = hide, replace = replace)
}

@JvmName("navigateParameterList")
public fun Router.navigate(to: String, parameters: Map<String, List<String>>, hide: Boolean = false) {
navigate(to, Parameters.from(parameters), hide = hide)
public fun Router.navigate(
to: String,
parameters: Map<String, List<String>>,
hide: Boolean = false,
replace: Boolean = false
) {
navigate(to, Parameters.from(parameters), hide = hide, replace = replace)
}

public fun Router.navigate(to: String, parameters: Map<String, String>, hide: Boolean = false) {
navigate(to, Parameters.from(parameters), hide = hide)
public fun Router.navigate(
to: String,
parameters: Map<String, String>,
hide: Boolean = false,
replace: Boolean = false
) {
navigate(to, Parameters.from(parameters), hide = hide, replace = replace)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class MockRouter : Router {
override fun getPath(initPath: String) =
derivedStateOf { currentState.value ?: initPath }

override fun navigate(to: String, hide: Boolean) {
override fun navigate(to: String, hide: Boolean, replace: Boolean) {
currentState.value = to
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package app.softwork.routingcompose
internal actual typealias Window = org.w3c.dom.Window

internal actual typealias History = org.w3c.dom.History
internal actual fun History.pushState(data: Any?, title: String, url: String?) {
this.pushState(data, title, url)
internal actual fun History.pushState(title: String, url: String?) {
this.pushState(null, title, url)
}
internal actual fun History.replaceState(title: String, url: String?) {
this.replaceState(null, title, url)
}

internal actual typealias Location = org.w3c.dom.Location
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public fun BrowserRouter(
initPath: String,
routeBuilder: @Composable RouteBuilder.() -> Unit
) {
BrowserRouter().route(initPath, routeBuilder)
Router.internalGlobalRouter = BrowserRouter().apply { route(initPath, routeBuilder) }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the use-case/code for setting this variable? Can't the function just return the Router instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could. I've implemented it this way just to simplify the client code and to be consistent with Router.current. In my applications I just use Router.global when Router.current can't be called.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean with can't be called? Because Router.current is @Composable or because there is no value set?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can't be called from any non-composable functions like effects, event handlers, view model methods or some global utility functions.

}

internal class BrowserRouter : Router {
Expand All @@ -47,11 +47,15 @@ internal class BrowserRouter : Router {

private fun Location.newPath() = "$pathname$search"

override fun navigate(to: String, hide: Boolean) {
override fun navigate(to: String, hide: Boolean, replace: Boolean) {
if (hide) {
currentLocation.value = to
} else {
window.history.pushState(null, "", to)
if (replace) {
window.history.replaceState("", to)
} else {
window.history.pushState("", to)
}
/*
The history API unfortunately provides no callback to listen to
[window.history.pushState], so we need to notify subscribers when pushing a new path.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public fun HashRouter(
initPath: String,
routeBuilder: @Composable RouteBuilder.() -> Unit
) {
HashRouter().route(initPath, routeBuilder)
Router.internalGlobalRouter = HashRouter().apply { route(initPath, routeBuilder) }
}

internal class HashRouter : Router {
Expand All @@ -38,7 +38,7 @@ internal class HashRouter : Router {
.removePrefix("/")
.ifBlank { null }

override fun navigate(to: String, hide: Boolean) {
override fun navigate(to: String, hide: Boolean, replace: Boolean) {
if (hide) {
currentHash.value = to.currentURL() ?: ""
} else if (window.location.hash.currentURL() == to.currentURL()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ internal expect var Window.onpopstate: () -> Unit
internal expect var Window.onhashchange: () -> Unit

internal expect abstract class History
internal expect fun History.pushState(data: Any?, title: String, url: String?)
internal expect fun History.pushState(title: String, url: String?)
internal expect fun History.replaceState(title: String, url: String?)

internal expect abstract class Location {
var pathname: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ internal class DesktopRouter : Router {
}
}

override fun navigate(to: String, hide: Boolean) {
override fun navigate(to: String, hide: Boolean, replace: Boolean) {
stack.add(Entry(to, hide))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ internal actual typealias Window = org.w3c.dom.Window

internal actual typealias History = org.w3c.dom.History

internal actual fun History.pushState(data: Any?, title: String, url: String?) {
this.pushState(data, title, url)
internal actual fun History.pushState(title: String, url: String?) {
this.pushState(null, title, url)
}

internal actual fun History.replaceState(title: String, url: String?) {
this.replaceState(null, title, url)
}

internal actual typealias Location = org.w3c.dom.Location
Expand Down
Loading