FYI: `routing-compose:0.1.7` is out, with support ...
# compose-web
h
FYI:
routing-compose:0.1.7
is out, with support for redirection if the routing tree changed. Simple use case: You got a link which requires a login first. https://github.com/hfhbd/routing-compose
🔥 1
🤘 3
K 8
Copy code
val isLoggedIn by remember { mutableStateOf(false) }
if (isLoggedIn) {
    route("/foo") {
        Text("Foo")
    }
} else {
    noMatch {
        Button(onClick = { isLoggedIn = true }) { Text("Login")) }
    }
}
Trying to access
/foo
without login results into showing the
Login
button, because
route("/foo")
is not available. After toggling
isLoggedIn = true
, this route is available, and visible.
👍 1
r
Experimenting with this... I'm having trouble doing routing at multiple levels of the tree via
Router.current
in the sub-tree? Is this supposed to be supported?
h
Do you have a sample?
r
I'm trying to use it like this:
Copy code
@Composable Top() {
  BrowserRouter(initRoute = "/") {
    route("/") {
      Child()
    }
  }
}

@Composable Child() {
  Router.current("/foo") {
    route("/foo") {
      ...
    }
    route("/bar") {
      ...
    }
  }
}
I've tried a few different variations but in effect I'm trying to have Child() manage its own routing within its sub-tree
h
You should not use
current.invoke()
and use only one router. What do you expect using different routers? What should happen, if calling
navigate(to)
?
r
How would I call
current { ... }
i.e. get a NavBuilder on the current router? The
initRoute
parameter is not optional.
h
NavBuilder.current
is currently not supported, and I don't think it will be in the future, because this class creates the route.
CompositionLocalProvider
is a tree based dependency injection, so although it is technically possible to update the
CompositionLocalProvider
in each call of
route/int
etc., I dont think this is a good idea. In generally, you should avoid using
CompositionLocalProvider.current
. Instead just use
@Composable NavBuilder.Child() { }
or use
@Composable Child(navBuilder: NavBuilder) { }
quote from Jim Sproch: As a side note, I'd also suggest treading lightly around CompositionLocals.  They create implicit data dependencies that are hard to analyze statically, lead to code that is challenging to debug+maintain, hard to share code across apps/codebases, hard to refactor, etc. You should generally avoid them like you avoid Globals or ThreadLocals. Just had some decent conversations on twitter about the topic here: https://twitter.com/JimSproch/status/1415139250283040772
r
Sounds good.
The call to
Router.current.navigate
I guess must be aware of the entire hierarchy right? i.e. the Child must know about the paths of the parent elements to call
navigate
on one of its own routes?
Another question... when dealing with invalid URLs -- I have
noMatch
if I actually want to display content, but what if I want to just navigate away immediately? I was thinking about some kind of
onNoMatch
callback where I can trigger a state change that would trigger the navigateTo, but maybe I'm approaching this the wrong way?
Another use case for -^ is forwarding/navigating on initial routing. For example, if I navigate to
/top
have the router automatically forward me to
/top/a
. I can do that now by checking
window.location
but doesn't seem like it should be necessary.
h
Well, currently, you need to know the full hierarchy. Give
/a
and
/top
and
/top/a
as possible routes. You are at
/top
. when you call
navigate(to="/a")
, you must distinguish to go to different routes, either via a second parameter or via
Router.current
and
Router.root
🤔
What about
noMatch { LaunchedEffect(Unit) { Router.current.navigate(to="/top/a") } }
r
For knowing the hierarchy, what about supporting relative paths? So one could do
navigateTo(to="foo")
for a route at the same "level" as a current one, or
navigateTo(to="../foo/bar")
for a child of a parent route.
noMatch { LaunchedEffect(Unit) { Router.current.navigate(to="/top/a") } }
doesn't work as-is because
navigateTo
is an
@Composable
invocation. This pattern seems to work, but looks very boiler-platey:
Copy code
val (navigateTo, setNavigateTo) = remember { mutableStateOf<String?>(null) }
if (navigateTo != null) {
  setNavigateTo(null)
  Router.current.navigate(to = navigateTo)
}
...
noMatch {
  LaunchedEffect(Unit) { setNavigateTo("/whatever") }
}
h
Router.current
is
@Composable
, navigateTo isn't
Copy code
val router = Router.current
LaunchedEffect(Unit) {
    router.navigate(to = "a")
}
But relative paths also requires knowing the full hierarchy. I dont like parsing relative paths 😄 So one option would be using another parameter in
navigate
, eg:
navigate(to="a", relative = true)
r
Router.current is @Composable, navigateTo isn't
Ah, ok. Still a bit messy to have to use that saved router reference, but better for sure.
But relative paths also requires knowing the full hierarchy.
Why do you say that?
I dont like parsing relative paths 😄 So one option would be using another parameter in navigate, eg: navigate(to="a", relative = true)
That would work.
h
Copy code
route("/foo") {
  Text("Foo")
  route("/a) {
    Text("A")
  }
  noMatch {
    Text("Else")
  }
}
Well, the current design of
routing-compose
is in fact a (recursive) functional `when`:
Copy code
val currentPath = currentURL.currentPath
when(currentPath) {
is "foo" -> {
    Text("Foo")
     val currentPath = currentPath.currentPath
     when(currentPath) {
     is "/a" -> {
         Text("A")
     }
     else (noMatch) -> {
        Text("Else") 
       }
     }
}
is Int ->...
}
So to support
navigate(to = "a", relative = true)
, you need an instance of
NavBuilder
in the correct sub tree, otherwise it is impossible to resolve the relative path! Actual, the Router does not know the current state (subtree) of
NavBuilder
, but this must be implemented in an update. So in each subtree, it is necessary to update
NavBuilder.current
(does not exists yet) and map it to
Router.current
... This requires a lot using
CompositionLocal
(which requires
current @Composable getter
by design). You should only use
Router.current
to get a reference deep in your UI, and this value is only set once at the start of your application.
NavBuilder.current
needs to be updated every recomposition, in every child Composable, only to "save" a parameter. Workaround: Use a reference: Receiver
@Composable fun NavBuilder.A()
or parameter
@Composable fun A(navBuilder: NavBuilder)
and full paths.
r
I'm still confused. I am passing a NavBuilder as receiver or parameter based on your previous comments.
You should only use Router.current to get a reference deep in your UI, and this value is only set once at the start of your application.
But
NavBuilder
has no reference to
Router
. It can define routes, but AFAICT I still need to do
Router.current
to use
navigate
.
h
Yes, this is expected, because there is no
relative
support yet 😄
r
Ah you were thinking through the relative implementation, got you
h
But you could simple add an extension:
inline val NavBuilder.router @Composable get() = Router.current
If you like to call
NavBuilder.router
instead
r
Sure, but I think here that just obfuscates things unnecessarily.
One other thing I'm struggling with is updating state based on the current route. For example, lets say I have a tab sheet with possible states: • no tab active • first tab active • second tab active Each state maps to a route. My first route is "no tab active". Then I click on the first tab, so state becomes "first tab active" and the route updates. Now I use the back button. The nav updates but the tab sheet state still has "first tab active". How can I make a state change in response to the nav change?
I see
getPath
but unsure of why that takes an
initPath
h
I guess, you are talking about a header with a navigation bar and some content in the main container? This is a little bit tricky, because normally Compose optimizes the updates: Updating the main content or even only the affected sub children. To update your navigation bar, you need to pass your current active route to the navbar by using
LaunchedEffect
. See https://github.com/hfhbd/ComposeTodo/pull/464/files for a sample
r
Yes, that's it. Will try that.
I have a case where the back button isn't working. How can I debug that?
h
Only by printing
NavBuilder
in each route...
AFAIK debugger doesnt work with IR and 3rd party libraries
r
To update your navigation bar, you need to pass your current active route to the navbar by using LaunchedEffect.
This doesn't work consistently.
LaunchedEffect
is not always triggered.
Ah, its because my tab manages its own current tab, and since it doesn't recompose the entire thing when the tab changes, LaunchedEffect only triggers once.
Using the url segment as the parameter to
LaunchedEffect
seems to work:
Copy code
string { urlSegment ->
  LaunchedEffect(urlSegment) {
    setCurrentTab(tabs.singleOrNull { it.urlSegment == urlSegment })
  }
  ...
}
The other thing that would be nice is an option for
navigate
to not push the navigation onto the back stack.
The main use case I have for that so far is when redirecting from an invalid route, the invalid route should not go to the back stack (using web).
Does the
@Routing
annotation do anything?
h
It provides a beautiful coloring 😄
😁 1