TL;DR: skip to the JUST SHOW ME THE STEPS ALREADY !!! section
That is the normal behaviour of the fragments. They are suppose to be recreated every time they are removed or replaced and you are suppose to restore their states using onSaveInstanceState
.
Here is a nice article that describes how to do it : Saving Fragment States
Other than that you can use View Model which is the part of the following recommended android architecture. They are a great way to retain and restore UI data.
You can learn how to implement this architecture by following this step by step code lab
EDIT : Solution
It took a while but here it is. Solution doesn't uses ViewModels
at the moment.
Read carefully because every step is important. This solution covers the following two parts
- Implement proper navigation on back key press
- Keeping fragment alive during navigation
Background :
Android Navigation component provides a NavController
class that you use to navigate between different destinations. Internally NavController
uses a Navigator
that actually does the navigation. Navigator
is a abstract class and anyone can extend/inherit this class to create there own custom navigator to provide custom navigation depending on the type of destination. When using fragments as destinations the NavHostFragment
uses a FragmentNavigator
whose default implementation replaces the fragments whenever we navigate using FragmentTransaction.replace()
which completely destroys previous fragment and adds new fragment. So we have to create our own navigator and instead of using FragmentTransaction.replace()
we will use a combination of FragmentTransaction.hide()
and FragmentTransaction.show()
to avoid fragment from being destroyed.
Default behavior of Navigation UI :
By default whenever you navigate to any other fragment other than the home/starting fragment they won't get added to backstack so lets say if you select fragments in the following order
A -> B -> C -> D -> E
your back stack will have only
[A, E]
as you can see the fragments B, C, D weren't added to backstack so pressing back press will always get you to fragment A which is the home fragment
The behavior we want for now :
We want a simple yet effective behavior. We wan't all fragments to get added to backstack but if the fragment is already in backstack we want to pop all fragments upto the selected fragment.
Lets say I select fragment in following order
A -> B -> C -> D -> E
the backstack should also be
[A, B, C, D, E]
upon pressing back only the last fragment should be popped and backstack should be like this
[A, B, C, D]
but if we navigate to lets say fragment B, since B is already in the stack then all the fragments above B should be popped and our backstack should look like this
[A, B]
I hope this behavior makes sense. This behavior is easy to implement using global actions as you will see below and is better than the default one.
OK Hotshot! now what ? :
Now we have two options
- extend
FragmentNavigator
- copy/paste
FragmentNavigator
Well I personally wanted to just extend FragmentNavigator
and override navigate()
method but since all its member variables are private I couldn't implement proper navigation.
So I decided to copy paste the entire FragmentNavigator
class and just change the name in entire code from "FragmentNavigator" to whatever I want to call it.
JUST SHOW ME THE STEPS ALREADY !!! :
- Create custom navigator
- Use custom tag
- Add global actions
- Use global actions
- Add the custom navigator to the NavController
STEP 1: Create custom navigator
Here is my custom navigator called StickyCustomNavigator
. All the code is same as FragmentNavigator
except the navigate()
method. As you can see it uses hide()
, show()
and add()
method instead of replace()
. The logic is simple. Hide the previous fragment and show the destination fragment. If this is our first time going to a specific destination fragment then add the fragment instead of showing it.
@Navigator.Name("sticky_fragment")
public class StickyFragmentNavigator extends Navigator<StickyFragmentNavigator.Destination> {
private static final String TAG = "StickyFragmentNavigator";
private static final String KEY_BACK_STACK_IDS = "androidx-nav-fragment:navigator:backStackIds";
private final Context mContext;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final FragmentManager mFragmentManager;
private final int mContainerId;
@SuppressWarnings("WeakerAccess") /* synthetic access */
ArrayDeque<Integer> mBackStack = new ArrayDeque<>();
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean mIsPendingBackStackOperation = false;
private final FragmentManager.OnBackStackChangedListener mOnBackStackChangedListener =
new FragmentManager.OnBackStackChangedListener() {
@SuppressLint("RestrictedApi")
@Override
public void onBackStackChanged() {
// If we have pending operations made by us then consume this change, otherwise
// detect a pop in the back stack to dispatch callback.
if (mIsPendingBackStackOperation) {
mIsPendingBackStackOperation = !isBackStackEqual();
return;
}
// The initial Fragment won't be on the back stack, so the
// real count of destinations is the back stack entry count + 1
int newCount = mFragmentManager.getBackStackEntryCount() + 1;
if (newCount < mBackStack.size()) {
// Handle cases where the user hit the system back button
while (mBackStack.size() > newCount) {
mBackStack.removeLast();
}
dispatchOnNavigatorBackPress();
}
}
};
public StickyFragmentNavigator(@NonNull Context context, @NonNull FragmentManager manager,
int containerId) {
mContext = context;
mFragmentManager = manager;
mContainerId = containerId;
}
@Override
protected void onBackPressAdded() {
mFragmentManager.addOnBackStackChangedListener(mOnBackStackChangedListener);
}
@Override
protected void onBackPressRemoved() {
mFragmentManager.removeOnBackStackChangedListener(mOnBackStackChangedListener);
}
@Override
public boolean popBackStack() {
if (mBackStack.isEmpty()) {
return false;
}
if (mFragmentManager.isStateSaved()) {
Log.i(TAG, "Ignoring popBackStack() call: FragmentManager has already"
+ " saved its state");
return false;
}
if (mFragmentManager.getBackStackEntryCount() > 0) {
mFragmentManager.popBackStack(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
mIsPendingBackStackOperation = true;
} // else, we're on the first Fragment, so there's nothing to pop from FragmentManager
mBackStack.removeLast();
return true;
}
@NonNull
@Override
public StickyFragmentNavigator.Destination createDestination() {
return new StickyFragmentNavigator.Destination(this);
}
@NonNull
public Fragment instantiateFragment(@NonNull Context context,
@SuppressWarnings("unused") @NonNull FragmentManager fragmentManager,
@NonNull String className, @Nullable Bundle args) {
return Fragment.instantiate(context, className, args);
}
@Nullable
@Override
public NavDestination navigate(@NonNull StickyFragmentNavigator.Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
if (mFragmentManager.isStateSaved()) {
Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
+ " saved its state");
return null;
}
String className = destination.getClassName();
if (className.charAt(0) == '.') {
className = mContext.getPackageName() + className;
}
final FragmentTransaction ft = mFragmentManager.beginTransaction();
int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
}
String tag = Integer.toString(destination.getId());
Fragment primaryNavigationFragment = mFragmentManager.getPrimaryNavigationFragment();
if(primaryNavigationFragment != null)
ft.hide(primaryNavigationFragment);
Fragment destinationFragment = mFragmentManager.findFragmentByTag(tag);
if(destinationFragment == null) {
destinationFragment = instantiateFragment(mContext, mFragmentManager, className, args);
destinationFragment.setArguments(args);
ft.add(mContainerId, destinationFragment , tag);
}
else
ft.show(destinationFragment);
ft.setPrimaryNavigationFragment(destinationFragment);
final @IdRes int destId = destination.getId();
final boolean initialNavigation = mBackStack.isEmpty();