3

What is the proper way of navigating back from nested fragments of ViewPager2?

Despite using app:defaultNavHost="true"with FragmentContainerView pressing back button while in a nested fragment of a page calls Activity's back press instead of navigating back to previous fragment.

Thracian
  • 43,021
  • 16
  • 133
  • 222

3 Answers3

9

As per the Create a NavHostFragment documentation, app:defaultNavHost="true" calls setPrimaryNavigationFragment() when the Fragment is first added - it is setPrimaryNavigationFragment() that routes back button press events to that fragment automatically.

In a ViewPager2 though, it is the ViewPager2 that is responsible for creating and adding the Fragment. Since every level of the Fragment hierarchy needs to be the primary navigation fragment, adding a child fragment via XML still doesn't solve the missing link: that the Fragment in the ViewPager2 needs to be the primary navigation fragment.

Therefore, you need to hook into the callbacks for when a Fragment is made the active Fragment and call setPrimaryNavigationFragment(). ViewPager2 1.1.0-alpha01 adds exactly this API in the FragmentTransactionCallback, specifically, the onFragmentMaxLifecyclePreUpdated(), which is called whenever the Lifecycle state of a Fragment is changed: when it is changed to RESUMED, that Fragment is now the active fragment and should become the primary navigation Fragment as part of the onPost callback.

private class Adapter(parentFragment: Fragment) : FragmentStateAdapter(parentFragment) {
    init {
        // Add a FragmentTransactionCallback to handle changing
        // the primary navigation fragment
        registerFragmentTransactionCallback(object : FragmentTransactionCallback() {
            override fun onFragmentMaxLifecyclePreUpdated(
                    fragment: Fragment,
                    maxLifecycleState: Lifecycle.State
            ) = if (maxLifecycleState == Lifecycle.State.RESUMED) {
                // This fragment is becoming the active Fragment - set it to
                // the primary navigation fragment in the OnPostEventListener
                OnPostEventListener {
                    fragment.parentFragmentManager.commitNow {
                        setPrimaryNavigationFragment(fragment)
                    }
                }
            } else {
                super.onFragmentMaxLifecyclePreUpdated(fragment, maxLifecycleState)
            }
        })
    }

    // The rest of your FragmentStateAdapter...
}
ianhanniballake
  • 191,609
  • 30
  • 470
  • 443
  • This is actually what i was looking for in the first place, since i couldn't able add navigation to `FragmentStateAdapter` i had to put them to it's `NavHostFragments`. – Thracian Jun 29 '20 at 06:33
  • It works if NavHostFragment is set `app:defaultNavHost="true"`in xml otherwise it didn't work. Also when `ViewPager2` itself was in a `NavHostFragment` you should set default navigation to true for both parent and page NavHostFragments. – Thracian Jun 29 '20 at 07:52
1

You need to override your parent activity's onBackPressed logic, you need to use https://developer.android.com/reference/androidx/navigation/NavController#popBackStack() to navigate up in your nav graph of nested fragment.

Filip Sollár
  • 91
  • 1
  • 5
1

Here is the Java version of @ianhanniballake answer (yes im a luddite for not using kotlin yet - i need to know java inside out first before i learn anything else). I havent unit tested this yet, but it "works"...

public class ViewPagerAdapter extends FragmentStateAdapter {
    public ViewPagerAdapter(@NonNull FragmentManager fragmentManager,
                            @NonNull Lifecycle lifecycle) {
        super(fragmentManager, lifecycle);

        registerFragmentTransactionCallback(new FragmentTransactionCallback() {
            @NonNull
            @Override
            public OnPostEventListener onFragmentMaxLifecyclePreUpdated(@NonNull Fragment fragmentArg,
                                                                        @NonNull Lifecycle.State maxLifecycleState) {
                if (maxLifecycleState == Lifecycle.State.RESUMED) {
                    return () -> fragmentArg.getParentFragmentManager().beginTransaction()
                            .setPrimaryNavigationFragment(fragmentArg).commitNow();
                } else {
                    return super.onFragmentMaxLifecyclePreUpdated(fragmentArg, maxLifecycleState);
                }
            }
        });
    }

    // remainder of your FragmentStateAdapter here

}
tommytucker7182
  • 213
  • 1
  • 5
  • 11