Explicit navigation¶
When Implicit navigation doesn't fit your use case, you can try an explicit approach.
Relevant methods
- ParentNode.attachChild()
- ParentNode.waitForChildAttached()
Using these methods we can chain together a path which leads from the root of the tree to a specific Node.
Use case¶
We want to navigate from Chat

to onboarding's first screen O1:

This time we'll want to do this explicitly by calling a function.
The plan¶
- Create a public method on
Rootthat attachesOnboarding - Create a public method on
Onboardingthat attaches the first onboarding screen - Create a
Navigator, that starting from an instance ofRoot, can chain these public methods together into a single action:navigateToO1() - Capture an instance of
Rootto use withNavigator - Call
navigateToO1()on ourNavigatorinstance
Step 1 – Root → Onboarding¶
First, we need to define how to programmatically attach Onboarding to the Root:
class RootNode(
buildContext: BuildContext,
backStack: BackStack<NavTarget>
) : ParentNode<NavTarget>(
buildContext = buildContext,
navModel = backStack,
) {
suspend fun attachOnboarding(): OnboardingNode {
return attachChild {
backStack.replace(NavTarget.Onboarding)
}
}
}
Let's break down what happens here:
- Since
attachChildhas a generic<T>return type, it will conform to the definedOnboardingNodetype - However,
attachChilddoesn't know how to create navigation toOnboardingNode– that's something only we can do with the provided lambda - We replace
NavTarget.Onboardinginto the back stack - Doing this should result in
OnboardingNodebeing created and added toRootNodeas a child attachChildexpects an instance ofOnboardingNodeto appear as a child ofRootas a consequence of executing our lambda- Once it appears,
attachChildreturns it
Important
It's our responsibility to make sure that the provided lambda actually results in the expected child being added. If we accidentally do something else instead, for example:
suspend fun attachOnboarding(): OnboardingNode {
return attachChild {
backStack.replace(NavTarget.Main) // Wrong NavTarget
}
}
Then an exception will be thrown after a timeout.
Step 2 – Onboarding → O1¶
Unlike Root, Onboarding uses Spotlight instead of BackStack as a NavModel, so navigation to the first screen is slightly different:
class OnboardingNode(
buildContext: BuildContext,
spotlight: Spotlight<NavTarget>
) : ParentNode<NavTarget>(
buildContext = buildContext,
navModel = spotlight,
) {
suspend fun attachO1(): O1Node {
return attachChild {
spotlight.activate(index = 0)
}
}
}
Step 3 – Our Navigator¶
interface Navigator {
fun navigateToO1()
}
In this case we'll implement it directly with our activity:
class ExplicitNavigationExampleActivity : NodeActivity(), Navigator {
lateinit var rootNode: RootNode // See the next step
override fun navigateToO1() {
lifecycleScope.launch {
rootNode
.attachOnboarding()
.attachO1()
}
}
}
Step 4 – An instance of RootNode¶
As the last piece of the puzzle, we'll also need to capture the instance of RootNode to make it all work. We can do that by a NodeReadyObserver plugin when setting up our tree:
class ExplicitNavigationExampleActivity : NodeActivity(), Navigator {
lateinit var rootNode: RootNode
override fun navigateToO1() { /*...*/ }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NodeHost(integrationPoint = appyxIntegrationPoint) {
RootNode(
buildContext = it,
navigator = this@ExplicitNavigationExampleActivity,
plugins = listOf(object : NodeReadyObserver<RootNode> {
override fun init(node: RootNode) {
rootNode = node
}
})
)
}
}
}
}
Step 5 – Using the Navigator¶
See how in the previous snippet RootNode receives a navigator dependency.
It can pass it further down the tree as a dependency to other nodes. Those nodes can call the methods of the Navigator, which will change the global navigation state directly.
Bonus: Wait for a child to be attached¶
There might be cases when we want to wait for a certain action to be performed by the user, rather than us, to result in a child being attached.
In these cases we can use ParentNode.waitForChildAttached() instead.
Use case – Wait for login¶
A typical case building an explicit navigation chain that relies on Logged in being attached. Most probably Logged in has a dependency on some kind of a User object. Here we want to wait for the user to authenticate themselves, rather than creating a dummy user object ourselves.
class RootNode(
buildContext: BuildContext,
) : ParentNode<NavTarget>(
buildContext = buildContext
) {
suspend fun waitForLoggedIn(): LoggedInNode =
waitForChildAttached<LoggedInNode>()
}
This method will wait for LoggedInNode to appear in the child list of RootNode and return with it. If it's already there, it returns immediately.
A navigation chain using it could look like:
class ExplicitNavigationExampleActivity : NodeActivity(), Navigator {
override fun navigateToProfile() {
lifecycleScope.launch {
rootNode
.waitForLoggedIn()
.attachMain()
.attachProfile()
}
}
}
You can find related code examples in ExplicitNavigationExampleActivity in our samples.