Aditya Kurkure
Aditya Kurkure

Reputation: 422

OnDestory() being called when switching back to fragment with NavigationUI

I am using NavigationUI with bottom navigation view and have also set it up with the action bar.

My app starts with the home fragment and when I switch to my search fragment OnDestroyView() is called on the homeFragment (expected) but when I switch back from the search fragment to my home fragment onDestroy() is called and then a new instance of homefragment is displayed rather than the one that it resuming at the state I left it in. How do I get it to resume at the state it was before?

This is how I have set up my NavigationUI:

NavController navController = Navigation.findNavController(this,
         R.id.navigation_host_fragment);

NavigationUI.setupWithNavController(mainBinding.mainActivityBn, navController);
NavigationUI.setupActionBarWithNavController(this,navController);

My Nav Graph:

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_activity_navigation"
app:startDestination="@id/homeFragment">

  <fragment
    android:id="@+id/homeFragment"
    android:name=<name>
    android:label="Home"
    tools:layout="@layout/fragment_home" />

  <fragment
    android:id="@+id/serachFragment"
    android:name=<name>
    android:label="Search"
    tools:layout="@layout/fragment_search" />

 </navigation>

Edit: I am using Bottom Navigation View to navigate between the fragments. When I press the back button the home fragment is loaded as it was in the previous state (expected behaviour).

Upvotes: 1

Views: 1637

Answers (1)

ianhanniballake
ianhanniballake

Reputation: 200080

As per this issue:

To understand the logic here, we need to look at what NavigationUI does when you select a destination. Looking at the source code and removing the unrelated code (animations, secondary menu code):

NavOptions.Builder builder = new NavOptions.Builder()
  .setLaunchSingleTop(true);
builder.setPopUpTo(findStartDestination(navController.getGraph()).getId(), false);
NavOptions options = builder.build();
navController.navigate(item.getItemId(), null, options);

So the popUpTo pops up to the root destination, but uses false to not pop the root destination itself. It then uses setLaunchSingleTop(true) to avoid having two instances of the start destination on the back stack. The intent here is to not clear any ViewModel or SavedStateHandle associated with that root destination when you go back to it.

So NavController calls navigate(), we pop everything up to but not including the start destination (your first screen) off the back stack, correctly removing the second screen.

NavController then calls through to FragmentNavigator to do the navigate() call. FragmentNavigator sees the launchSingleTop is true and correctly determines that the fragment is already on the top of the stack. However, Fragments, unlike activities, do not have any equivalent to onNewIntent() for onNewArguments() for mimicking the singleTop behavior, so FragmentNavigator pops the previous fragment and adds a new instance of the Fragment, passing in the new arguments (here, just null).

So due to how Fragments handle the setLaunchSingleTop(true) flag, clicking the first item in the bottom nav destroys and recreates the Fragment again. This is why the system back button acts differently - the system back button is just calling popBackStack() and not using any of the single top behavior.

That issue is still open and indicates that:

The behavior of FragmentNavigator with single top is bad and should feel bad. It does not match the behavior of activities and is unintuitive. If you don't mind, let's use this bug to track that work. Ideally, a new instance would not be created, meaning your original MapFragment would just be resumed and would be right back where it was. This would be consistent with what happens when you hit the system back button.

There's no direct workaround to that issue, so the best result (not having this happen) would require that you star that issue and wait for it be fixed. However, you'll note that any state or ViewModels you save into your NavBackStackEntry would be saved over a singleTop operation:

// Instead of using this:
myViewModel = new ViewModelProvider(this).get(MyViewModel.class);

// You could use:
NavController navController = NavHostFragment.findNavController(this);
NavBackStackEntry entry = navController.getBackStackEntry(R.id.homeFragment);
myViewModel = new ViewModelProvider(entry).get(MyViewModel.class);

However, this would not help with saving the Fragment's view state (i.e., scroll position) since those aren't possible to save into the NavBackStackEntry, but are only saved at the Fragment level.

Upvotes: 2

Related Questions