androidandroid-fragmentskotlinnavigation-architecture

How to handle multiple NavHosts/NavControllers?


I'm having a problem when dealing with multiple NavHosts. This issue is very similar to the one asked here. I think the solution for this question would help me as well, but it's a post from 2017 and it still has no answer. Android Developers Documentation doesn't help and searching through the web shows absolutely nothing that could possibly help.

So basically I have one Activity and two Fragments. Let's call them FruitsActivity, FruitListFragment, FruitDetailFragment, where FruitsActivity has no relevant code and its xml layout is composed by a <fragment> tag, serving as NavHost, like that:

<fragment
            android:id="@+id/fragmentContainer"
            android:name="androidx.navigation.fragment.NavHostFragment"
            app:navGraph="@navigation/fruits_nav_graph"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

The FruitListFragment is the startDestination of my NavGraph, it handles a list of fruits that will come from the server. The FruitDetailFragment shows details about the Fruit selected in the list displayed by FruitListFragment.

So far we have Activity -> ListFragment -> DetailFragment.

Now I need to add one more Fragment, called GalleryFragment. It's a simple fragment that displays many pictures of the Fruit selected and it's called by FruitDetailFragment when clicking a button.

The problem here is: In Portrait mode I simply use findNavController().navigate(...) and I navigate through the Fragments like I want. but when I'm using a Tablet in Landscape mode, I'm using that Master Detail Flow to display List and Details on the same screen. There is an example of how it works here, and I want the GalleryFragment to replace the FruitDetailFragment, sharing the screen with the list of fruits, but so far I could only manage to make it replace the "main navigation" flow, occupying the entire screen and sending the FruitListFragment to the Back Stack.

I already tried to play around with findNavController() method, but no matter from where I call it I can only get the same NavController all the time, and it always navigates in the same linear way. I tried to implement my own NavHost, but I get and error "class file for androidx.navigation.NavHost not found".

This is the xml of my FruitsActivity:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <fragment
            android:id="@+id/fragmentContainer"
            android:name="androidx.navigation.fragment.NavHostFragment"
            app:navGraph="@navigation/listing_nav_graph"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </FrameLayout>

</layout>

This is the xml of the FruitListActivity in Landscape mode (in portrait mode there is just the RecyclerView):

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/rvFruits"
                android:layout_weight="3"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_margin="8dp"/>

            <FrameLayout
                android:layout_weight="1"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
                <fragment
                    android:id="@+id/sideFragmentContainer"
                    android:name="fruits.example.FruitDetailFragment"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"/>
            </FrameLayout>

        </LinearLayout>

    </RelativeLayout>

</layout>

And now I want to call the GalleryFragment and make it replace just the <fragment> of id 'sideFragmentContainer' instead of the whole screen (instead of replacing the <fragment> of id fragmentContainer in the Activity's xml).

I didn't find any explanations of how to handle multiple NavHosts or <fragment> inside <fragment>.

So based on that, is it possible to use Navigation Architecture and display a Fragment inside another Fragment? Do I need multiple NavHosts for that, or is there another way?


Solution

  • As suggested by jsmyth886, this blog post pointed me to the right direction. The trick was to findFragmentById() to get the NavHost directly from the fragment container (in this case, the one sharing the screen with the rest of the Master Detail screen). This allowed me to access the correct NavController and navigate as expected. It's important to create a second NavGraph too.

    So a quick step-by-step:

    
        <fragment
                    android:id="@+id/fragmentContainer"
                    android:name="androidx.navigation.fragment.NavHostFragment"
                    app:navGraph="@navigation/main_nav_graph"
                    app:defaultNavHost="true"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"/>
    
    
    
        <?xml version="1.0" encoding="utf-8"?>
            <layout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:app="http://schemas.android.com/apk/res-auto">
    
                <RelativeLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent">
    
                    <LinearLayout
                        android:orientation="horizontal"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent">
    
                        <androidx.recyclerview.widget.RecyclerView
                            android:id="@+id/rvFruits"
                            android:layout_weight="3"
                            android:layout_width="match_parent"
                            android:layout_height="match_parent"
                            android:layout_margin="8dp"/>
    
                        <FrameLayout
                            android:layout_weight="1"
                            android:layout_width="match_parent"
                            android:layout_height="match_parent">
                            <fragment
                                android:id="@+id/sideFragmentContainer"
                                android:name="androidx.navigation.fragment.NavHostFragment"
                                app:navGraph="@navigation/secondary_nav_graph"
                                app:defaultNavHost="false"
                                android:layout_width="match_parent"
                                android:layout_height="match_parent"/>
                        </FrameLayout>
    
                    </LinearLayout>
    
                </RelativeLayout>
    
            </layout>
    
    
    val navHostFragment = childFragmentManager.findFragmentById(R.id. sideFragmentContainer) as NavHostFragment
    

    That's it, simpler than what I thought. Thank you Lara Martín for the blog post and jsmyth886 for pointing me to the right direction!