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
Root
that attachesOnboarding
- Create a public method on
Onboarding
that 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
Root
to use withNavigator
- Call
navigateToO1()
on ourNavigator
instance
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
attachChild
has a generic<T>
return type, it will conform to the definedOnboardingNode
type - However,
attachChild
doesn't know how to create navigation toOnboardingNode
– that's something only we can do with the provided lambda - We replace
NavTarget.Onboarding
into the back stack - Doing this should result in
OnboardingNode
being created and added toRootNode
as a child attachChild
expects an instance ofOnboardingNode
to appear as a child ofRoot
as a consequence of executing our lambda- Once it appears,
attachChild
returns 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.