androidkotlinandroid-fragmentschild-fragmentnavcontroller

childFragmentManager.findFragmentById(R.id.nav_host_fragment_challenges) returns null


Thanks to a bottomNavigationView, I use NavController to navigate between 4 different fragments.
In one of these fragments (ChallengesFragment) I have an inner_fragment (nav_host_fragment_challenges) but when I try to navigate in this inner_fragment it doesn't work.

Here's my MainActivity.kt :

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        val navController = navHostFragment.navController

        val bottomNavigationView = findViewById<BottomNavigationView>(R.id.navigation_bar)
        bottomNavigationView.setupWithNavController(navController)
    }
}

My ChallengesFragment.kt (fragment that host the inner_fragment) :

class ChallengesFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {

        val inflater_challenges = LayoutInflater.from(ContextThemeWrapper(context, R.style.Theme_challenges))
        val view = inflater_challenges.inflate(R.layout.fragment_challenges, container, false)

        return view
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val challengesNavHostFragment = childFragmentManager.findFragmentById(R.id.nav_host_fragment_challenges) as NavHostFragment
        //val challengesNavHostFragment = requireActivity().supportFragmentManager.findFragmentById(R.id.nav_host_fragment_challenges) as NavHostFragment
        val challengesNavController = challengesNavHostFragment.navController
    }
}

My problem : val challengesNavHostFragment = childFragmentManager.findFragmentById(R.id.nav_host_fragment_challenges) as NavHostFragment give me this error: java.lang.NullPointerException: null cannot be cast to non-null type androidx.navigation.fragment.NavHostFragment
So I tried val challengesNavHostFragment = requireActivity().supportFragmentManager.findFragmentById(R.id.nav_host_fragment_challenges) as NavHostFragment (line with // in the code) and it works at first but when I leave ChallengesFragment and then come back the inner_fragment doesn't show up anymore...
I think the way to go is with childFragmentManager but if you have a solution that works with something else I take it.

The FragmentContainerView of my fragment_challenges.xml :

<androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment_challenges"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/challenge_topbar"
        app:navGraph="@navigation/nav_graph_challenges"
        app:defaultNavHost="false"
        tools:layout="@layout/fragment_quests" />

My nav_graph.xml if needed :

<?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"
    android:id="@+id/nav_graph"
    app:startDestination="@id/homeFragment">

    <fragment
        android:id="@+id/profileFragment"
        android:name="fr.apgames.veuja.fragments.ProfileFragment"
        android:label="ProfileFragment" />
    <fragment
        android:id="@+id/calendarFragment"
        android:name="fr.apgames.veuja.fragments.CalendarFragment"
        android:label="CalendarFragment" />
    <fragment
        android:id="@+id/challengesFragment"
        android:name="fr.apgames.veuja.fragments.ChallengesFragment"
        android:label="ChallengesFragment" />
    <fragment
        android:id="@+id/homeFragment"
        android:name="fr.apgames.veuja.fragments.HomeFragment"
        android:label="HomeFragment" />
    <include app:graph="@navigation/nav_graph_challenges" />
</navigation>

And finally my nav_graph_challenges.xml :

<?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"
    android:id="@+id/nav_graph_challenges"
    app:startDestination="@id/challengesQuestsFragment">

    <fragment
        android:id="@+id/challengesMyStoryFragment"
        android:name="fr.apgames.veuja.fragments.ChallengesMyStoryFragment"
        android:label="ChallengesMyStoryFragment" >
        <action
            android:id="@+id/action_challengesMyStoryFragment_to_challengesQuestsFragment"
            app:destination="@id/challengesQuestsFragment" />
    </fragment>
    <fragment
        android:id="@+id/challengesQuestsFragment"
        android:name="fr.apgames.veuja.fragments.ChallengesQuestsFragment"
        android:label="ChallengesQuestsFragment" >
        <action
            android:id="@+id/action_challengesQuestsFragment_to_challengesMyStoryFragment"
            app:destination="@id/challengesMyStoryFragment" />
    </fragment>
</navigation>

Solution

  • The inflater passed into onCreateView not the same LayoutInflater you would get from LayoutInflater.from(context) - it is aware of what Fragment you are in. That's how the FragmentContainerView knows that it needs to be added as a child fragment of the Fragment you're in (if you look at the source code, you'll see the special casing for FragmentContainerView).

    So when you use

    val inflater_challenges = LayoutInflater.from(
        ContextThemeWrapper(context, R.style.Theme_challenges))
    

    You are creating a LayoutInflater that is completely unaware of what fragment you are on. This is why your fragment (which should be a child fragment) actually gets added to the Activity's FragmentManager. That has the side effect of not actually nesting the fragments within one another, thus causing the fragment to not re-appear when the parent becomes visible again (because from FragmentManager's perspective, they are completely independent fragments).

    This kind of issue would fire a WrongNestedHierarchyViolation if you enabled StrictMode for fragments specifically because of the negative side effects of not nesting your fragments correctly.

    So you should:

    1. Use the inflater passed to you by onCreateView
    2. Use android:theme="@style/Theme.challenges" in your fragment_challenges.xml XML file to set the theme on everything in that part of the hierarchy, rather than using ContextThemeWrapper.