1

I'm looking for a generic way to create my custom fragment with that has OnBackPressedCallback and viewModel that extends NavHostFragment using navigation graph i intend to put as child fragments into it's back stack.

Normally i create NavHostFragment for each tab or fragment with their FragmentContainerView, it's easy but repetitive to create for each host with

fragment_nav_host_home.xml

<?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">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.fragment.app.FragmentContainerView
            android:id="@+id/nested_nav_host_fragment_home"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"

            app:defaultNavHost="false"
            app:navGraph="@navigation/nav_graph_home"/>

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

and writing databinding for their layouts, setting and using id nested_nav_host_fragment_home

class HomeNavHostFragment : BaseDataBindingFragment<FragmentNavhostHomeBinding>() {
    override fun getLayoutRes(): Int = R.layout.fragment_navhost_home

    private val appbarViewModel by activityViewModels<AppbarViewModel>()

    private var navController: NavController? = null

    private val nestedNavHostFragmentId = R.id.nested_nav_host_fragment_home


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


        val nestedNavHostFragment =
            childFragmentManager.findFragmentById(nestedNavHostFragmentId) as? NavHostFragment
        navController = nestedNavHostFragment?.navController


        // Listen on back press
        listenOnBackPressed()

    }

    private fun listenOnBackPressed() {
        requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
    }


    override fun onResume() {
        super.onResume()
        callback.isEnabled = true

        // Set this navController as ViewModel's navController
        appbarViewModel.currentNavController.value = navController

    }

    override fun onPause() {
        super.onPause()
        callback.isEnabled = false
    }

    val callback = object : OnBackPressedCallback(false) {

        override fun handleOnBackPressed() {

            // Check if it's the root of nested fragments in this navhost
            if (navController?.currentDestination?.id == navController?.graph?.startDestination) {

                /*
                    Disable this callback because calls OnBackPressedDispatcher
                     gets invoked  calls this callback  gets stuck in a loop
                 */
                isEnabled = false
                requireActivity().onBackPressed()
                isEnabled = true

            } else {
                navController?.navigateUp()
            }

        }
    }

}

Instead i tried to write more generic class for creating NavHostFragment directly instead of putting it inside another fragment

class BaseNavHostFragment private constructor() : NavHostFragment() {

    private val appbarViewModel by activityViewModels<AppbarViewModel>()

    companion object {
        fun create(
            @NavigationRes navGraphId: Int,
            startDestinationArgs: Bundle? = null
        ): BaseNavHostFragment {
            return NavHostFragment.create(navGraphId, startDestinationArgs) as BaseNavHostFragment
        }
    }

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


    private fun listenOnBackPressed() {
        requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
    }


    override fun onResume() {
        super.onResume()
        callback.isEnabled = true
        // Set this navController as ViewModel's navController
        appbarViewModel.currentNavController.value = navController

    }

    override fun onPause() {
        super.onPause()
        callback.isEnabled = false
    }


    /**
     * This callback should be created with Disabled because on rotation ViewPager creates
     * NavHost fragments that are not on screen, destroys them afterwards but it might take
     * up to 5 seconds.
     *
     * ### Note: During that interval touching back button sometimes call incorrect [OnBackPressedCallback.handleOnBackPressed] instead of this one if callback is **ENABLED**
     */
    private val callback = object : OnBackPressedCallback(false) {

        override fun handleOnBackPressed() {

            // Check if it's the root of nested fragments in this nav host
            if (navController?.currentDestination?.id == navController?.graph?.startDestination) {

                /*
                    Disable this callback because calls OnBackPressedDispatcher
                     gets invoked  calls this callback  gets stuck in a loop
                 */
                isEnabled = false
                requireActivity().onBackPressed()
                isEnabled = true

            } else {
                navController?.navigateUp()
            }
        }
    }
}

When i call create function it returns ClassCastException. Any way to create a NavHostFragment only passing R.navitation.x is simple way for creating to add or replace with fragment manager or ViewPager2, but couldn't find how to create a fragment this way.

4
  • Why are you doing any of this mess with OnBackPressedDispatcher? NavHostFragment already handles that for you if you're calling setPrimaryNavigationFragment() as per the documentation Commented Jun 28, 2020 at 20:41
  • @ianhanniballake Because i use these fragments with a ViewPager2, without setting custom back behavior app gets destroyed instead of navigating to previous fragment of current NavHostFragment. And every step of it is required to handle back press properly. Commented Jun 28, 2020 at 21:04
  • So your problem is that you aren't calling setPrimaryNavigationFragment with the current page as it changes in your ViewPager2. Maybe you should instead ask a question on how to do that instead. Commented Jun 28, 2020 at 21:55
  • @ianhanniballake i tried setting `app:defaultNavHost="true" any and every page of the ViewPager before implementing the custom behavior but it didn't work, is there something i missed out? But my question is not that, if there is another way to implement back behavior with ViewPager2 it's better, i also need to have a ViewModel inside this NavHostFragment to be able set back arrow behavior of Appbar too. So my question is to create custom NavHostFragment Commented Jun 28, 2020 at 22:12

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.