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.

Thracian
  • 43,021
  • 16
  • 133
  • 222
  • 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](https://developer.android.com/guide/navigation/navigation-programmatic#create_a_navhostfragment) – ianhanniballake Jun 28 '20 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. – Thracian Jun 28 '20 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. – ianhanniballake Jun 28 '20 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 – Thracian Jun 28 '20 at 22:12

0 Answers0