rozina
rozina

Reputation: 4232

Fragment lifecycle overlap on navigate

I have a single Activity application with multiple Fragments that are being switched by using Navigation components. When I switch between two fragments their onCreate() and onDestroy() methods seem to overlap. Thus making it difficult for me to write initialization and clean up code for fragments when they access the same global objects.

Navigating from Framgent_A to Fragment_B has the following order of methods:

Fragment_B.onCreate()
Fragment_A.onDestroy()

In Fragment_A.onDestroy() I reverse the operations I do in Fragment_A.onCreate(). And in Fragment_B I expect things to be in a neutral state when onCreate() is called. However that is not the case since Fragment_A.onDestroy() has not yet been called.

Is the overlap normal on Android or did I configure something wrong in my Navigation components? Is there another way I could achieve what I am trying to do? I know I could couple both Fragments and make it work, but I don't want either Fragment to know about each other. To me it seems weird that Framgnet_A is still alive when Fragment_B is created, when Fragment_B is supposed to replace Fragment_A.

Any help is greatly appreciated!


Edit:

After groing through the source code while debugging I have found out that in FragmentNavigator.navigate() FragmentTransaction.setReorderingAllowed() is called, which allows reordering of operations, even allowing onCreate() of a new fragment to be called before onDestroy() of the previous. The question still remains, how can I solve my problem of correctly cleaning up global state in one Fragment before initializing the same global state in the next Fragment.

Upvotes: 17

Views: 6140

Answers (4)

Hussein El Feky
Hussein El Feky

Reputation: 6707

My case was a little bit different, and I would like to share it in case anyone faced the same issue.

I wanted to do an action in onPause() of the current fragment, but not execute that code when one navigates from a fragment to another. What I had to do was to call isRemoving() method to check if the current fragment is being removed or not. It is set to true when NavController.navigate(...) method is called.

override fun onPause() {
    super.onPause()
    if (!isRemoving()) {
        // Write your code here
    }
}

Per Google's Fragment.isRemoving() documentation:

Return true if this fragment is currently being removed from its activity. This is not whether its activity is finishing, but rather whether it is in the process of being removed from its activity.

Upvotes: 0

B. Plüster
B. Plüster

Reputation: 654

Perhaps you could move some the code related to initialization where you assume a neutral state to that fragments onStart() or onCreateView() method. According to the developer documentation this is where initialization should take place.

Another option available is using an Observer /Observable pattern, where you could notify your Activity once onDestroy() in Fragment A is completed. The Activity would then notify Fragment B that it is safe to assume a cleaned up state and begin initialization.

Upvotes: 2

tynn
tynn

Reputation: 39863

The Android Fragment life-cycle is not really an appropriate callback host for your needs. The navigation controller will replace the two fragments with animation, so both are somehow visible the same time and eventually even onPause() of the exiting fragment is called after onResume() of the entering one.

Solution 1: Use OnDestinationChangedListener

The onDestinationChanged() callback is called before any of the life-cycle events. As a very simplified approach (look out for leaks) you could do the following:

    findNavController().addOnDestinationChangedListener { _, destination, _ ->
        if(shouldCleanupFor(destination)) cleanup()
    }

Solution 2: Abstract the global changes away

Instead of having single navigation points change the global state, have a single point of truth for it. This could be another fragment independent of the navigation hierarchy. This then observes the navigation as before:

findNavController(R.id.nav_graph).addOnDestinationChangedListener { _, destination, _ ->
    resetAll()
    when(distination.id) {
        R.id.fragment_a -> prepareForA()
        R.id.fragment_b -> prepareForB()
        else -> prepareDefault()
    }
}

As an additional advantage you could implement the state changes idempotently as well.

Upvotes: 8

skryshtafovych
skryshtafovych

Reputation: 582

Since you have an activity that controls the inflation of your Fragments you can manually control the lifecycles of the fragment that are being inflated. By calling into below methods you can control which fragment is ready to use global data. You will at this point have to, some how pass data back to Mainactivity to establish which fragment is active since your asking about how to inflate 2 fragment simultaneously which will share an object. Better approach would be to have the MainActivity implement FragmentA and FragmentB-detail with specific classes to do Stuff this way you have to treat your app like Tablet and determine 2 pane mode and which point you can use appropriate classes out of those fragments controlled by your Activity. The included link matches what your trying to accomplish

private void addCenterFragments(Fragment fragment) {
        try {
            removeActiveCenterFragments();
            fragmentTransaction = fragmentManager.beginTransaction();
            fragmentTransaction.add(R.id.content_fragment, fragment);
            fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
            activeCenterFragments.add(fragment);
            fragmentTransaction.commit();
        }catch (Exception e){
            Crashlytics.logException(e);
        }
    }

    private void removeActiveCenterFragments() {
        if (activeCenterFragments.size() > 0) {
            fragmentTransaction = fragmentManager.beginTransaction();
            for (Fragment activeFragment : activeCenterFragments) {
                fragmentTransaction.remove(activeFragment);
            }
            activeCenterFragments.clear();
            fragmentTransaction.commit();
        }
    }

Upvotes: 2

Related Questions