Files
coco 723ce1af5c a
2026-07-03 15:12:48 +08:00

210 lines
6.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Explicit navigation
When [Implicit navigation](implicit-navigation.md) doesn't fit your use case, you can try an explicit approach.
!!! info "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`
<img src="https://i.imgur.com/jqWOHhJ.png" width="450">
to onboarding's first screen `O1`:
<img src="https://i.imgur.com/MWgLOWy.png" width="450">
This time we'll want to do this explicitly by calling a function.
## The plan
1. Create a public method on `Root` that attaches `Onboarding`
2. Create a public method on `Onboarding` that attaches the first onboarding screen
3. Create a `Navigator`, that starting from an instance of `Root`, can chain these public methods together into a single action: `navigateToO1()`
4. Capture an instance of `Root` to use with `Navigator`
5. Call `navigateToO1()` on our `Navigator` instance
## Step 1 `Root` → `Onboarding`
First, we need to define how to programmatically attach `Onboarding` to the `Root`:
```kotlin
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:
1. Since `attachChild` has a generic `<T>` return type, it will conform to the defined `OnboardingNode` type
2. However, `attachChild` doesn't know how to create navigation to `OnboardingNode` that's something only we can do with the provided lambda
3. We replace `NavTarget.Onboarding` into the back stack
4. Doing this _should_ result in `OnboardingNode` being created and added to `RootNode` as a child
5. `attachChild` expects an instance of `OnboardingNode` to appear as a child of `Root` as a consequence of executing our lambda
6. Once it appears, `attachChild` returns it
!!! info "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:
```kotlin
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](../navmodel/spotlight.md) instead of [BackStack](../navmodel/backstack.md) as a `NavModel`, so navigation to the first screen is slightly different:
```kotlin
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`
```kotlin
interface Navigator {
fun navigateToO1()
}
```
In this case we'll implement it directly with our activity:
```kotlin
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:
```kotlin
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.
```kotlin
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:
```kotlin
class ExplicitNavigationExampleActivity : NodeActivity(), Navigator {
override fun navigateToProfile() {
lifecycleScope.launch {
rootNode
.waitForLoggedIn()
.attachMain()
.attachProfile()
}
}
}
```
You can find related code examples in `ExplicitNavigationExampleActivity` in our samples.