Unfortunately, solutions based on NavigationUtils, that use only findNode() method of NavGraph class, have one serious downside - it is not possible to navigate to destination that points to current NavGraph itself. Another words, findNode() method will find nothing.
As an example action action_startingFragment_to_startingFragment in the graph below can be considered:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/some_graph"
app:startDestination="@id/startingFragment">
<fragment
android:id="@+id/startingFragment"
android:name="com.xxx.StartingFragment">
<action
android:id="@+id/action_startingFragment_to_startingFragment"
app:destination="@id/some_graph"
app:popUpTo="@id/startingFragment"
app:popUpToInclusive="true" />
</fragment>
</navigation>
To take into account mentioned case, it is also required to check if found current node itself is a target destination or not.
So the extension function will look like the following:
fun NavController.navigateSafe(@IdRes actionId: Int, args: Bundle?) {
currentDestination?.let { currentDestination ->
val navAction = currentDestination.getAction(actionId)
// to navigate successfully certain action should be explicitly stated in nav graph
if (navAction != null) {
val destinationId = navAction.destinationId
if (destinationId != 0) {
val currentNode = currentDestination as? NavGraph ?: currentDestination.parent
if (currentNode?.id == destinationId || <--------- THIS CONDITION IS THE KEY
currentNode?.findNode(destinationId) != null
) {
navigate(actionId, args, null)
}
}
}
}
}
UPDATE:
Also more interesting case is lacking, when destination points to parent NavGraph itself like below.
Parent graph:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_graph"
app:startDestination="@id/mainFragment">
<fragment
android:id="@+id/mainFragment"
android:name="com.xxx.main.MainFragment">
<action
android:id="@+id/action_main_to_some"
app:destination="@id/some_graph"/>
</fragment>
<include app:graph="@navigation/some_graph" />
</navigation>
Child graph:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/some_graph"
app:startDestination="@id/someFragment">
<fragment
android:id="@+id/someFragment"
android:name="com.xxx.some.SomeFragment">
<action
android:id="@+id/action_some_to_main"
app:destination="@id/main_graph"
app:popUpTo="@id/someFragment"
app:popUpToInclusive="true" />
</fragment>
</navigation>
Action action_some_to_main is target one.
Therefore, extension function need to be modified:
fun NavController.navigateSafe(@IdRes actionId: Int, args: Bundle?) {
currentDestination?.let { currentDestination ->
val navAction = currentDestination.getAction(actionId)
// to navigate successfully certain action should be explicitly stated in nav graph
if (navAction != null) {
val destinationId = navAction.destinationId
if (destinationId != 0) {
val currentNode = currentDestination as? NavGraph ?: currentDestination.parent
if (currentNode?.findDestination(destinationId) != null) { <----- CHANGED HERE
navigate(actionId, args, null)
}
}
}
}
}
private fun NavGraph.findDestination(destinationId: Int): NavDestination? {
if (id == destinationId) return this
val node = findNode(destinationId)
if (node != null) return node
return parent?.findDestination(destinationId)
}