Lesson 13 / 17
Navigation and Routes
Measuring that a destination chosen by a route is not in the UI tree until navigation finishes, so the argument sent to it goes outside first and then down, and the result returned goes outside.
Published Updated
In this lesson
- Measures the result of a model that parses a route and selects the destination
- Shows that an argument sent to the destination before navigation finishes stays outside the tree
- Distinguishes that the same argument goes down once the destination enters the tree
- Explains that the result carried on the way back is outside at the moment of navigation
Contents
Until now both ends of every measured move were in the tree. Navigation changes this. When the user moves from a list to a product detail, the detail screen is not in the tree before the transition. The product number to be carried to it, however, is known at the start of the transition. The data exists before its destination.
This lesson measures how the navigation host (NavHost) closes this gap. The route selects the destination; the destination enters the tree; the argument arrives then. The subject of the lesson is that the same argument carries two different directions at two moments.
// Direction.kt — classifies which direction a value travels in the UI tree
class Tree(private val parentOf: Map<String, String>) {
fun contains(node: String): Boolean = node in parentOf.keys || node in parentOf.values
fun parent(node: String): String? = parentOf[node]
fun ancestors(node: String): List<String> {
val chain = mutableListOf<String>()
var current = parentOf[node]
while (current != null) {
chain.add(current)
current = parentOf[current]
}
return chain
}
}
class Move(val label: String, val from: String, val to: String, tree: Tree) {
val direction: String = when {
!tree.contains(from) || !tree.contains(to) -> "outside"
to in tree.ancestors(from) -> "up"
tree.parent(to) == from -> "down"
from in tree.ancestors(to) -> "deep"
else -> error("unrelated: $from and $to")
}
fun describe() {
println("$label: from=$from | to=$to -> $direction")
}
}
fun countDirections(moves: List<Move>) {
for (name in listOf("down", "up", "deep", "outside")) {
println("$name: ${moves.count { it.direction == name }}")
}
}
Route, Destination and Back Stack
The navigation setup consists of three parts. The navigation controller (NavController) keeps which destination you are at and the back stack. The navigation host (NavHost) stands in the tree and draws the content of the current destination as its child. A route is the text address of a destination; the argument is carried inside the route: detail/42 carries the argument 42 to the detail destination.
// Platform code: a navigation host, routes and a destination with an argument
@Composable
fun AppNavigation(navController: NavHostController) {
NavHost(navController, startDestination = "list") {
composable("list") {
ProductList(onOpen = { id -> navController.navigate("detail/$id") })
}
composable("detail/{id}") { entry ->
ProductDetail(entry.arguments?.getString("id"))
}
}
}
When navigate("detail/42") is called, the platform parses the route, finds the matching destination, adds a new entry to the back stack and changes the host’s content. The destination’s content enters the tree only after these steps. Until that moment ProductDetail has not been drawn anywhere.
The model captures this platform rule; the model is not the platform itself. The helper file parses the route and returns a destination name, and it builds a tree in which a destination sits under the navigation host.
// Routes.kt — route parsing and destination selection (the model's calculation)
class Route(val destination: String, val argument: String)
fun parseRoute(route: String): Route {
val parts = route.split("/")
return Route(parts[0], parts.getOrElse(1) { "" })
}
fun routeTarget(route: String, graph: List<String>): String? {
val destination = parseRoute(route).destination
return graph.firstOrNull { it.lowercase() == destination }
}
fun navTree(current: String): Tree =
Tree(mapOf("Screen" to "App", "NavHost" to "Screen", current to "NavHost"))
Before Navigation
In the first measurement the list destination is drawn in the tree. The user wants to go to the route detail/42. The model parses the route and selects the destination; but the tree is still the tree from before navigation.
// Main.kt — the argument sent to the destination before navigation starts
fun main() {
val graph = listOf("List", "Detail")
val route = "detail/42"
val target = routeTarget(route, graph) ?: return
println("target: $target")
println("argument: ${parseRoute(route).argument}")
val before = navTree("List")
Move("argument", "NavHost", target, before).describe()
}
target: Detail argument: 42 argument: from=NavHost | to=Detail -> outside
The destination Detail was selected and the argument 42 was parsed; but because Detail is not in the tree, the move came out outside. The data is ready, the receiver is not. In this interval the platform keeps the argument in the entry on the back stack; when drawing starts, the destination reads it from there.
LC1. The model carries at most one argument in a route and leaves it as text. The platform’s argument types, default values and parsing of query parameters in the route are not in the model.
After Navigation
In the second measurement navigation is complete: there is now a Detail under the host. The same argument goes from the same host to the same destination.
// Main.kt — the argument after navigation finishes
fun main() {
val graph = listOf("List", "Detail")
val target = routeTarget("detail/42", graph) ?: return
val after = navTree(target)
Move("argument", "NavHost", target, after).describe()
}
argument: from=NavHost | to=Detail -> down
The same two ends, down this time. Because the destination entered the tree, it is a direct child of the host. Only the tree changed: the data, the source and the target are the same.
LC2. The model builds navigation as a single step, as the tree passing from its before to its after in one instant. The old and new destinations being in the tree at the same time during the platform’s transition animation is not in the model.
The Result Coming Back
Navigation is not one-way. When the user returns from the detail, the detail screen leaves the tree and the list is drawn again. If the detail screen left a result, for example that the product was added to the cart, this result reaches the list through the entry on the back stack. At the moment the result is written the detail screen is still in the tree, but when the list reads it the detail is long gone.
// Main.kt — the result the detail left on the way back
fun main() {
val back = navTree("List")
Move("result", "Detail", "List", back).describe()
}
result: from=Detail | to=List -> outside
Both ends of this line are written by hand: this line is written to show the rule; it is not a measurement. That the returned result is outside also rests on the result line in the bounding measurement, whose sender end is the output of routeTarget.
Outside. When the return is complete, the sending node is not in the tree. The result is carried not over the tree but over the entry the navigation controller keeps. The argument is outside on the way forward, the result on the way back; both depend on an end that is not in the tree on one side of the transition.
LC3. The model measures the result only from the moment of return. The moment the result is written while the detail screen is still in the tree was not built as a separate move in the model.
The Bounding Measurement
The lesson’s direction comes from how the argument is split before and after navigation. The measurement below shows four moves together: the start route the screen gives the navigation host, the argument before navigation, the argument after navigation and the result coming back.
// Main.kt — the four moves of navigation
fun main() {
val graph = listOf("List", "Detail")
val target = routeTarget("detail/42", graph) ?: return
val before = navTree("List")
val after = navTree(target)
val moves = listOf(
Move("start-route", "Screen", "NavHost", before),
Move("argument-before", "NavHost", target, before),
Move("argument-after", "NavHost", target, after),
Move("result", target, "List", before),
)
for (move in moves) {
move.describe()
}
countDirections(moves)
}
start-route: from=Screen | to=NavHost -> down argument-before: from=NavHost | to=Detail -> outside argument-after: from=NavHost | to=Detail -> down result: from=Detail | to=List -> outside down: 2 up: 0 deep: 0 outside: 2
Both ends of the start-route line are written by hand: this line is written to show the rule; it is not a measurement. One end of each of the three lines that count as measurements is the output of routeTarget: down 1, outside 2.
Two down, two outside. The start route and the argument after navigation went down because they are between two connected nodes in the tree. The argument before navigation and the returning result stayed outside because one end is not in the tree. Navigation exists to manage the two moves outside the tree; the direction depends on at which moment the move is measured. The lesson’s direction: outside.
LC4. The model takes the four moves from two moments of the tree. The time that passes between these moments on the platform, which entry the argument rests in, or what is preserved when the process restarts are not measured in this lesson.
Summary
- This lesson’s direction: outside. The argument sent before navigation and the returned result are tied to an end that is not in the tree.
- A route is the text address of a destination; the argument is carried inside the route.
- Before navigation finishes, the destination is not in the tree, so the argument going to it is outside.
- Once the destination enters the tree, the same argument goes down to the host’s child.
- The returning result is outside because it is read after the sender has left the tree.
Next Step
Navigation selects which content will be drawn, while the theme determines how every drawn piece of content will look. The next lesson measures how a theme chosen at the root descends all the way to the deep text, and how the direction changes when a subtree provides its own theme.
To keep your progress and take notes, Log in
My notes
Log in to take notes.