Search code examples
androidandroid-navigationandroid-navigation-graphandroid-deep-link

Android Advanced Navigation deeplinking between graphs


I have been using Android Advanced Navigation for a while. In my current project, I have three navigation graphs. The problem is, some of the fragments in one graph should be reached from another graph. In order to solve this, I've made deep links.

For example, in graph A I've included graph B and then used a deep link from graph B to reach that particular fragment. The problem is when I am in graph B and now I want to jump back to graph A I can't. As graph A is not included in graph B, the current navigation controller can't find the destination. If I include graph A in graph B, another problem occurs. Android Studio can't build the project as it is having a circular import problem (graph B tries to import graph A but graph B is already included in graph A etc.) and I really don't know what else to do.

I've tried creating one huge navigation graph which contains all three subgraphs but I couldn't make it work with this Android Advanced Navigation. Is there any more efficient way?

Edit to add code:

BottomNavigationView extension:

fun BottomNavigationView.setupWithNavController(
    navGraphIds: List<Int>,
    fragmentManager: FragmentManager,
    containerId: Int,
    intent: Intent
): LiveData<NavController> {

    // Map of tags
    val graphIdToTagMap = SparseArray<String>()
    // Result. Mutable live data with the selected controlled
    val selectedNavController = MutableLiveData<NavController>()

    var firstFragmentGraphId = 0

    // First create a NavHostFragment for each NavGraph ID
    navGraphIds.forEachIndexed { index, navGraphId ->
        val fragmentTag = getFragmentTag(index)

        // Find or create the Navigation host fragment
        val navHostFragment = obtainNavHostFragment(
            fragmentManager,
            fragmentTag,
            navGraphId,
            containerId
        )

        // Obtain its id
        val graphId = navHostFragment.navController.graph.id

        if (index == 0) {
            firstFragmentGraphId = graphId
        }

        // Save to the map
        graphIdToTagMap[graphId] = fragmentTag

        // Attach or detach nav host fragment depending on whether it's the selected item.
        if (this.selectedItemId == graphId) {
            // Update livedata with the selected graph
            selectedNavController.value = navHostFragment.navController
            attachNavHostFragment(fragmentManager, navHostFragment, index == 0)
        } else {
            detachNavHostFragment(fragmentManager, navHostFragment)
        }
    }

    // Now connect selecting an item with swapping Fragments
    var selectedItemTag = graphIdToTagMap[this.selectedItemId]
    val firstFragmentTag = graphIdToTagMap[firstFragmentGraphId]
    var isOnFirstFragment = selectedItemTag == firstFragmentTag

    // When a navigation item is selected
    setOnNavigationItemSelectedListener { item ->
        // Don't do anything if the state is state has already been saved.
        if (fragmentManager.isStateSaved) {
            false
        } else {
            val newlySelectedItemTag = graphIdToTagMap[item.itemId]
            if (selectedItemTag != newlySelectedItemTag) {
                // Pop everything above the first fragment (the "fixed start destination")
                fragmentManager.popBackStack(firstFragmentTag,
                    FragmentManager.POP_BACK_STACK_INCLUSIVE)
                val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag)
                        as NavHostFragment

                // Exclude the first fragment tag because it's always in the back stack.
                if (firstFragmentTag != newlySelectedItemTag) {
                    // Commit a transaction that cleans the back stack and adds the first fragment
                    // to it, creating the fixed started destination.
                    fragmentManager.beginTransaction()
                        .setCustomAnimations(
                            R.anim.nav_default_enter_anim,
                            R.anim.nav_default_exit_anim,
                            R.anim.nav_default_pop_enter_anim,
                            R.anim.nav_default_pop_exit_anim)
                        .attach(selectedFragment)
                        .setPrimaryNavigationFragment(selectedFragment)
                        .apply {
                            // Detach all other Fragments
                            graphIdToTagMap.forEach { _, fragmentTagIter ->
                                if (fragmentTagIter != newlySelectedItemTag) {
                                    detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!)
                                }
                            }
                        }
                        .addToBackStack(firstFragmentTag)
                        .setReorderingAllowed(true)
                        .commit()
                }
                selectedItemTag = newlySelectedItemTag
                isOnFirstFragment = selectedItemTag == firstFragmentTag
                selectedNavController.value = selectedFragment.navController
                true
            } else {
                false
            }
        }
    }

    // Optional: on item reselected, pop back stack to the destination of the graph
    setupItemReselected(graphIdToTagMap, fragmentManager)

    // Handle deep link
    setupDeepLinks(navGraphIds, fragmentManager, containerId, intent)

    // Finally, ensure that we update our BottomNavigationView when the back stack changes
    fragmentManager.addOnBackStackChangedListener {
        if (!isOnFirstFragment && !fragmentManager.isOnBackStack(firstFragmentTag)) {
            this.selectedItemId = firstFragmentGraphId
        }

        // Reset the graph if the currentDestination is not valid (happens when the back
        // stack is popped after using the back button).
        selectedNavController.value?.let { controller ->
            if (controller.currentDestination == null) {
                controller.navigate(controller.graph.id)
            }
        }
    }
    return selectedNavController
}

private fun BottomNavigationView.setupDeepLinks(
    navGraphIds: List<Int>,
    fragmentManager: FragmentManager,
    containerId: Int,
    intent: Intent
) {
    navGraphIds.forEachIndexed { index, navGraphId ->
        val fragmentTag = getFragmentTag(index)

        // Find or create the Navigation host fragment
        val navHostFragment = obtainNavHostFragment(
            fragmentManager,
            fragmentTag,
            navGraphId,
            containerId
        )
        // Handle Intent
        if (navHostFragment.navController.handleDeepLink(intent)
            && selectedItemId != navHostFragment.navController.graph.id) {
            this.selectedItemId = navHostFragment.navController.graph.id
        }
    }
}

private fun BottomNavigationView.setupItemReselected(
    graphIdToTagMap: SparseArray<String>,
    fragmentManager: FragmentManager
) {
    setOnNavigationItemReselectedListener { item ->
        val newlySelectedItemTag = graphIdToTagMap[item.itemId]
        val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag)
                as NavHostFragment
        val navController = selectedFragment.navController
        // Pop the back stack to the start destination of the current navController graph
        navController.popBackStack(
            navController.graph.startDestination, false
        )
    }
}

private fun detachNavHostFragment(
    fragmentManager: FragmentManager,
    navHostFragment: NavHostFragment
) {
    fragmentManager.beginTransaction()
        .detach(navHostFragment)
        .commitNow()
}

private fun attachNavHostFragment(
    fragmentManager: FragmentManager,
    navHostFragment: NavHostFragment,
    isPrimaryNavFragment: Boolean
) {
    fragmentManager.beginTransaction()
        .attach(navHostFragment)
        .apply {
            if (isPrimaryNavFragment) {
                setPrimaryNavigationFragment(navHostFragment)
            }
        }
        .commitNow()

}

private fun obtainNavHostFragment(
    fragmentManager: FragmentManager,
    fragmentTag: String,
    navGraphId: Int,
    containerId: Int
): NavHostFragment {
    // If the Nav Host fragment exists, return it
    val existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment?
    existingFragment?.let { return it }

    // Otherwise, create it and return it.
    val navHostFragment = NavHostFragment.create(navGraphId)
    fragmentManager.beginTransaction()
        .add(containerId, navHostFragment, fragmentTag)
        .commitNow()
    return navHostFragment
}

private fun FragmentManager.isOnBackStack(backStackName: String): Boolean {
    val backStackCount = backStackEntryCount
    for (index in 0 until backStackCount) {
        if (getBackStackEntryAt(index).name == backStackName) {
            return true
        }
    }
    return false
}

private fun getFragmentTag(index: Int) = "bottomNavigation#$index"

Navigation graphs:

<?xml version="1.0" encoding="utf-8"?>
<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/home_nav_graph"
    app:startDestination="@id/homeFragment">

    <fragment
        android:id="@+id/homeFragment"
        android:name="lu.thebiggame.fragments.home.HomeFragment"
        android:label="home_fragment"
        tools:layout="@layout/home_fragment" >
        <action
            android:id="@+id/homeToGameResults"
            app:destination="@id/gameResultsFragment" />
        <action
            android:id="@+id/homeToPlayLottoGames"
            app:destination="@id/playLottoGamesFragment" />
    </fragment>
    <fragment
        android:id="@+id/checkoutFragment"
        tools:layout="@layout/checkout_fragment"
        android:name="lu.thebiggame.fragments.checkout.CheckoutFragment"
        android:label="CheckoutFragment" />
    <fragment
        android:id="@+id/gameResultsFragment"
        android:name="lu.thebiggame.fragments.gameresults.GameResultsFragment"
        android:label="game_results_fragment"
        tools:layout="@layout/game_results_fragment">
        <deepLink
            android:id="@+id/gameResultsFragmentDeepLink"
            app:uri="app://home/game-results-fragment" />
        <action
            android:id="@+id/gameResultsToPlayLottoGames"
            app:destination="@id/playLottoGamesFragment" />
    </fragment>
    <fragment
        android:id="@+id/congratulationsFragment"
        android:name="lu.thebiggame.fragments.congratulations.CongratulationsFragment"
        android:label="congratulations_fragment"
        tools:layout="@layout/congratulations_fragment" >
        <action
            android:id="@+id/congratulationsToClaim"
            app:destination="@id/claimFragment" />
        <deepLink
            android:id="@+id/congratulationsFragmentDeepLink"
            app:uri="app://home/congratulations-fragment" />
    </fragment>
    <fragment
        android:id="@+id/claimFragment"
        tools:layout="@layout/claim_fragment"
        android:name="lu.thebiggame.fragments.claim.ClaimFragment"
        android:label="ClaimFragment" />

    <include app:graph="@navigation/profile_nav_graph"/>
    <fragment
        android:id="@+id/aboutGameFragment"
        android:name="lu.thebiggame.fragments.page.PageFragment"
        android:label="page_fragment"
        tools:layout="@layout/page_fragment">

        <deepLink
            android:id="@+id/aboutGameFragmentDeepLink"
            app:uri="app://home/about-game-fragment" />
    </fragment>
    <fragment
        android:id="@+id/playLottoGamesFragment"
        android:name="lu.thebiggame.fragments.play.lottogames.PlayLottoGamesFragment"
        tools:layout="@layout/play_lotto_games_fragment"
        android:label="PlayLottoGamesFragment" >
        <action
            android:id="@+id/playLottoGamesToCheckout"
            app:destination="@id/checkoutFragment" />
        <deepLink
            android:id="@+id/playLottoGamesDeepLink"
            app:uri="app://home/play-lotto-games-fragment" />
        <action
            android:id="@+id/playLottoGamesToGameResults"
            app:destination="@id/gameResultsFragment" />
    </fragment>

</navigation>
<?xml version="1.0" encoding="utf-8"?>
<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/games_nav_graph"
    app:startDestination="@id/gamesFragment">

    <fragment
        android:id="@+id/gamesFragment"
        android:name="lu.thebiggame.fragments.games.GamesFragment"
        android:label="games_fragment"
        tools:layout="@layout/games_fragment" />

    <include app:graph="@navigation/home_nav_graph"/>
</navigation>
<?xml version="1.0" encoding="utf-8"?>
<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/profile_nav_graph"
    app:startDestination="@id/profileFragment">

    <fragment
        android:id="@+id/profileFragment"
        android:name="lu.thebiggame.fragments.profile.ProfileFragment"
        android:label="profile_fragment"
        tools:layout="@layout/profile_fragment" >
        <action
            android:id="@+id/profileToResults"
            app:destination="@id/resultsFragment" />
        <action
            android:id="@+id/profileToHelp"
            app:destination="@id/helpFragment" />
        <action
            android:id="@+id/profileToPersonal"
            app:destination="@id/personalFragment" />
        <action
            android:id="@+id/profileToChangePassword"
            app:destination="@id/changePasswordFragment" />
        <action
            android:id="@+id/profileToPage"
            app:destination="@id/pageFragment" />
    </fragment>
    <fragment
        android:id="@+id/resultsFragment"
        android:name="lu.thebiggame.fragments.results.ResultsFragment"
        android:label="results_fragment"
        tools:layout="@layout/results_fragment" >
        <argument
            android:name="startingTab"
            app:argType="integer"
            android:defaultValue="0" />
        <deepLink
            android:id="@+id/resultsFragmentDeepLink"
            android:autoVerify="true"
            app:uri="app://profile/results-fragment" />
        <action
            android:id="@+id/resultsToClaim"
            app:destination="@id/claimFragmentResults" />
    </fragment>
    <fragment
        android:id="@+id/helpFragment"
        android:name="lu.thebiggame.fragments.help.HelpFragment"
        android:label="help_fragment"
        tools:layout="@layout/help_fragment" >
        <action
            android:id="@+id/helpToPage"
            app:destination="@id/pageFragment" />
        <action
            android:id="@+id/helpToContact"
            app:destination="@id/contactUsFragment" />
        <action
            android:id="@+id/helpToFaq"
            app:destination="@id/faqFragment" />
    </fragment>
    <fragment
        android:id="@+id/personalFragment"
        android:name="lu.thebiggame.fragments.personal.PersonalFragment"
        android:label="personal_fragment"
        tools:layout="@layout/personal_fragment" />
    <fragment
        android:id="@+id/changePasswordFragment"
        android:name="lu.thebiggame.fragments.changepassword.ChangePasswordFragment"
        android:label="change_password_fragment"
        tools:layout="@layout/change_password_fragment" />
    <fragment
        android:id="@+id/pageFragment"
        android:name="lu.thebiggame.fragments.page.PageFragment"
        android:label="page_fragment"
        tools:layout="@layout/page_fragment" />
    <fragment
        android:id="@+id/contactUsFragment"
        android:name="lu.thebiggame.fragments.contactus.ContactUsFragment"
        android:label="contact_us_fragment"
        tools:layout="@layout/contact_us_fragment" />
    <fragment
        android:id="@+id/faqFragment"
        android:name="lu.thebiggame.fragments.faqfragment.FaqFragment"
        android:label="faq_fragment"
        tools:layout="@layout/faq_fragment" />
    <fragment
        android:id="@+id/claimFragmentResults"
        tools:layout="@layout/claim_fragment"
        android:name="lu.thebiggame.fragments.claim.ClaimFragment"
        android:label="ClaimFragment" />


</navigation>

Initializing bottom navigation:

    private fun setupBottomNavigationBar() {

        activityMainBottomNav.itemIconTintList = null

        val navGraphIds = listOf(
            R.navigation.home_nav_graph,
            R.navigation.games_nav_graph,
            R.navigation.profile_nav_graph
        )

        val controller = activityMainBottomNav.setupWithNavController(
            navGraphIds = navGraphIds,
            fragmentManager = supportFragmentManager,
            containerId = R.id.nav_host_container,
            intent = intent
        )

        currentNavController = controller
    }

Solution

  • As nobody bothered to answer me, here is what I did. All the fragments that can appear in all three of my navigation graphs I moved to another graph and named it a shared graph. Furthemore, in order to navigate to this graph I created standard actions to it, but before navigating I made sure to change starting position of shared graph.

        fun navigateToNestedGraph(navDir: NavDirections, destination: Int, navOps: NavOptions? = null){
            val view = (this as Fragment).requireView()
            val navController = Navigation.findNavController(view)
            val graph = navController.graph.findNode(R.id.shared_nav_graph)
            if (graph is NavGraph){
                graph.startDestination = destination
                navController.navigate(navDir, navOps)
            }
        }
    

    This is a function from a listener that my Fragments implement. That's it.