Navigate Back with Navigation Component
Or how to do that without creating a new instance of Fragment
As we all know, Google introduced Navigation Components some time ago. And moreover there is already 2.1.0 version at the moment. But there is small problem: we can not go back to specific fragment without its recreation. So this small article will be about workaround, you can use to eliminate such flaw.
The problem
Suppose we have One Activity Several fragments navigation (by the way I wrote about such pattern previously, you can check that here). And suppose we have 3 fragments. Fragment one has its own state, which can be saved and restore, and fragment three contain a button to go back to the first one. You can check base project here(code) and here(xml).
To navigate from third fragment to first one we use navigation action with attribute popUpTo
:
<action
android:id="@+id/action_fragment_3_to_fragment_1"
app:destination="@id/fragment_1"
app:enterAnim="@anim/anim_slide_pop_exit_fragment"
app:exitAnim="@anim/anim_slide_exit_fragment"
app:popEnterAnim="@anim/anim_slide_pop_enter_fragment"
app:popExitAnim="@anim/anim_slide_pop_exit_fragment"
app:popUpTo="@+id/fragment_1"/>
To understand the current problem we need to change state of first fragment to be different from default one, then go to the third fragment, and then press a button to navigate back to the first one and BAM, you loose you saved state and have default one again:
Moreover if we slow our open/close fragment animations you will see, that the animation of going back to first fragment is not really smooth:
Understanding
That happens because first fragment is getting deleted and recreate from scratch. Let’s go to source code of Navigation library to check that.
Actually all basic navigation work starts, when you call NavController.navigate
. At this point 2 main steps happens (at least two main steps, that are important for us):
- Popup back stack until we reach fragment specified by
popUpTo
attribute - Create new fragment, specified by destinationId attribute
Eventually we will come to FragmentNavigator.navigate
method, which creates needed fragment inside. So actually without specifying popUpInclusive
attribute we will have new instance of Fragment1 with old instance, lying in the back stack. With using this attribute, old fragment will be permanently deleted and replaced by new one.
Solution
Unfortunately I didn’t found any elegant solution:
- Y̶o̶u̶ ̶c̶a̶n̶ ̶n̶o̶t̶ ̶a̶v̶o̶i̶d̶ ̶w̶r̶i̶t̶i̶n̶g̶ ̶d̶e̶s̶t̶i̶n̶a̶t̶i̶o̶n̶I̶d̶,̶ ̶s̶i̶n̶c̶e̶ ̶t̶h̶i̶s̶ ̶a̶t̶t̶r̶i̶b̶u̶t̶e̶ ̶i̶s̶ ̶r̶e̶q̶u̶i̶r̶e̶d̶ . Actually this is not true. You can!! And this is actual right solution. Check the UPD in the end of article
- You can not use any other attributes
- You can not adjust
FragmentNavigator
for your needs
One thing you can do, that will help us, is to specify your own Navigator. So let’s do that.
First, describe you NavHostFragment:
class MyNavHostFragment : NavHostFragment() {
override fun createFragmentNavigator() =
MyFragmentNavigator(requireContext(), childFragmentManager, id)
}
Next, create your own Navigator, that will inherit fragment navigator. The main change here will be checking wether popUpTo
is equal to destination
. And if yes, that probably means (at least in our project) that you just want to go back to that fragment without recreation, so you can just return navDest without changing:
@Navigator.Name("fragment")
class MyFragmentNavigator(
context: Context,
fm: FragmentManager,
containerId: Int
) : FragmentNavigator(context, fm, containerId) {
override fun navigate(...): NavDestination? {
val shouldSkip = navOptions?.run {
popUpTo == destination.id && !isPopUpToInclusive
} ?: false
return if (shouldSkip) null
else super.navigate(destination, args, navOptions, navigatorExtras)
}
}
Don’t forget to place @Navigator.Name
annotation on class with name fragment, so that your navigator will be used as FragmentNavigator instead of default one.
And the last thing override method of creating fragment navigator in NavHostFragment
. And use that fragment instead of default one in your xml our whatever place.
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/navFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="com.github.backnavigation.MyNavHostFragment"
app:defaultNavHost="true"
app:navGraph="@navigation/navigation" />
</FrameLayout>
Now lets check our flow with new awesome approach ;)
Update at the end
While I wrote that article, I’ve checked the source code of Navigation component one more time and realized, that I was completely blind :(
As appeared, you may not specify destination
attribute, but specify popUpTo
attribute. In that case NavController
will know, that you want just to go back till fragment, specified in popUpTo
attribute without creating anything else.
So the only one right solution in this problem, which I described at the beginning of the article, is:
<action
android:id="@+id/action_fragment_3_to_fragment_1"
app:enterAnim="@anim/anim_slide_pop_exit_fragment"
app:exitAnim="@anim/anim_slide_exit_fragment"
app:popEnterAnim="@anim/anim_slide_pop_enter_fragment"
app:popExitAnim="@anim/anim_slide_pop_exit_fragment"
app:popUpTo="@+id/fragment_1"/>
Instead of
<action
android:id="@+id/action_fragment_3_to_fragment_1"
app:destination="@id/fragment_1"
app:enterAnim="@anim/anim_slide_pop_exit_fragment"
app:exitAnim="@anim/anim_slide_exit_fragment"
app:popEnterAnim="@anim/anim_slide_pop_enter_fragment"
app:popExitAnim="@anim/anim_slide_pop_exit_fragment"
app:popUpTo="@+id/fragment_1"/>
So this whole article is becoming useless, but I just didn’t want to throw my little research away, considering the fact that I already wrote almost everything and I had no other material on that month. :(