16
votes

I came across inconsistent return values from Fragment.isRemoving() when the activity has just added the fragment to the back stack. The first time the fragment is temporarily destroyed due to configuration change, isRemoving() returns true. If the fragment is temporarily destroyed a second time, isRemoving() returns false!

My code:

public class MainActivityFragment extends Fragment {
    private static final String TAG = "MainActivityFragment";
    private static final String LEVEL = "MainActivityFragment.LEVEL";

    public MainActivityFragment() {
    }

    public static MainActivityFragment newInstance(int n) {
        MainActivityFragment f = new MainActivityFragment();
        f.setArguments(new Bundle());
        f.getArguments().putInt(LEVEL, n);
        return f;
    }

    private int getLevel() {
        return (getArguments() == null) ? 0 : getArguments().getInt(LEVEL);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_main, container, false);

        Button button = (Button) rootView.findViewById(R.id.button);

        button.setText(String.valueOf(getLevel()));

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                getActivity().getSupportFragmentManager()
                        .beginTransaction()
                        .replace(R.id.fragment, MainActivityFragment.newInstance(getLevel() + 1))
                        .addToBackStack(null)
                        .commit();
            }
        });

        return rootView;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.i(TAG, String.valueOf(getLevel()) + ": onCreate");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.i(TAG, String.valueOf(getLevel()) + ": onDestroy");
        Log.i(TAG, String.valueOf(getLevel()) + ": isChangingConfigurations() == " + getActivity().isChangingConfigurations());
        Log.i(TAG, String.valueOf(getLevel()) + ": isRemoving() == " + isRemoving());
    }

The log (lines starting with # are my comments):

# Start Activity
I/MainActivityFragment: 0: onCreate
# Click button in fragment 0 to add it to back stack and replace it with fragment 1
I/MainActivityFragment: 1: onCreate
# Rotate the device
I/MainActivityFragment: 0: onDestroy
I/MainActivityFragment: 0: isChangingConfigurations() == true
I/MainActivityFragment: 0: isRemoving() == true # ???????
I/MainActivityFragment: 1: onDestroy
I/MainActivityFragment: 1: isChangingConfigurations() == true
I/MainActivityFragment: 1: isRemoving() == false
I/MainActivityFragment: 0: onCreate
I/MainActivityFragment: 1: onCreate
# Rotate the device a second time
I/MainActivityFragment: 0: onDestroy
I/MainActivityFragment: 0: isChangingConfigurations() == true
I/MainActivityFragment: 0: isRemoving() == false # Correct result
I/MainActivityFragment: 1: onDestroy
I/MainActivityFragment: 1: isChangingConfigurations() == true
I/MainActivityFragment: 1: isRemoving() == false
I/MainActivityFragment: 0: onCreate
I/MainActivityFragment: 1: onCreate
# Click button in fragment 1 to add it to back stack and replace it with fragment 2
I/MainActivityFragment: 2: onCreate
# Rotate the device
I/MainActivityFragment: 0: onDestroy
I/MainActivityFragment: 0: isChangingConfigurations() == true
I/MainActivityFragment: 0: isRemoving() == false # Ok, correct
I/MainActivityFragment: 1: onDestroy
I/MainActivityFragment: 1: isChangingConfigurations() == true
I/MainActivityFragment: 1: isRemoving() == true # WHY????
I/MainActivityFragment: 2: onDestroy
I/MainActivityFragment: 2: isChangingConfigurations() == true
I/MainActivityFragment: 2: isRemoving() == false
I/MainActivityFragment: 0: onCreate
I/MainActivityFragment: 1: onCreate
I/MainActivityFragment: 2: onCreate

Is this a bug in Android or am I understanding this wrong?

Update: I added a call to Fragment.dump() in onDestroy and I got the following results:

Before the fragment is put in the back stack:

mFragmentId=#7f0c006b mContainerId=#7f0c006b mTag=null
mState=2 mIndex=0 mWho=android:fragment:0 mBackStackNesting=0
mAdded=true mRemoving=false mResumed=false mFromLayout=false mInLayout=false
mHidden=false mDetached=false mMenuVisible=true mHasMenu=false
mRetainInstance=false mRetaining=false mUserVisibleHint=true
mFragmentManager=FragmentManager{336d670b in HostCallbacks{387c69e8}}
mHost=android.support.v4.app.FragmentActivity$HostCallbacks@387c69e8
mSavedViewState={2131492979=android.view.AbsSavedState$1@6adf801}
  Child FragmentManager{2b6916a6 in null}}:
    FragmentManager misc state:
    mHost=null
    mContainer=null
    mCurState=0 mStateSaved=true mDestroyed=true

After the fragment is put in the back stack and is destroyed the first time:

mFragmentId=#7f0c006b mContainerId=#7f0c006b mTag=null
mState=1 mIndex=0 mWho=android:fragment:0 mBackStackNesting=1
mAdded=false mRemoving=true mResumed=false mFromLayout=false mInLayout=false
mHidden=false mDetached=false mMenuVisible=true mHasMenu=false
mRetainInstance=false mRetaining=false mUserVisibleHint=true
mFragmentManager=FragmentManager{34638ae1 in HostCallbacks{2db8e006}}
mHost=android.support.v4.app.FragmentActivity$HostCallbacks@2db8e006
mSavedViewState={2131492979=android.view.AbsSavedState$1@6adf801}
Child FragmentManager{169d66c7 in null}}:
  FragmentManager misc state:
    mHost=null
    mContainer=null
    mCurState=0 mStateSaved=true mDestroyed=true

Destroyed the second time:

mFragmentId=#7f0c006b mContainerId=#7f0c006b mTag=null
mState=1 mIndex=0 mWho=android:fragment:0 mBackStackNesting=1
mAdded=false mRemoving=false mResumed=false mFromLayout=false mInLayout=false
mHidden=false mDetached=false mMenuVisible=true mHasMenu=false
mRetainInstance=false mRetaining=false mUserVisibleHint=true
mFragmentManager=FragmentManager{23beb2bc in HostCallbacks{c0f9245}}
mHost=android.support.v4.app.FragmentActivity$HostCallbacks@c0f9245
mSavedFragmentState=Bundle[{android:view_state={2131492979=android.view.AbsSavedState$1@6adf801}}]
mSavedViewState={2131492979=android.view.AbsSavedState$1@6adf801}

The differences between the first (not in back stack yet) and second (put in back stack) are:

  1. mState=2 (ACTIVITY_CREATED) vs. mState=1 (CREATED)
  2. mBackStackNesting=0 vs. mBackStackNesting=1
  3. mAdded=true vs. mAdded=false
  4. mRemoving=false vs. mRemoving=true (obviously)

The differences between the second (first time destroyed) and third (second+ time destoyed) are:

  1. mRemoving=true vs. mRemoving=false
  2. mSavedFragmentState=null vs mSavedFragmentState=Bundle[...]
  3. has Child FragmentManager vs. has no Child FragmentManager

However, I have no idea how to interpret these results.

I'm starting to think isRemoving is not what I need (what I actually need is something equivalent to Activity.isFinishing but for fragments. I need to know that "this fragment will never be reused again", so I can cancel background tasks. Right now I'm using isRemoving() && !getActivity().isChangingConfigurations() but I'm not sure it's the right solution).

5
I have never played with isRemoving(). I agree that the results that you are seeing are very strange. You might consider using dump() on Fragment, to get more state information, instead of the logging that you're doing, to see if that gives you any more clues.CommonsWare
The thing is, you don't try to understand fragments!jimmy0251
@CommonsWare I updated the question with results of Fragment.dump()imgx64

5 Answers

7
votes

Original Not Quite Right Answer

I am not sure whether or not it is a bug or by design but a fragment is only ever set to removing in the FragmentManager.removeFragment method of the support v4 library v23.1.1.

This could very well be different depending on if you are using the support library and what version but for the code you have in the GitHub repo this is the reason.

This method is only ever called when a fragment is being removed that has been placed on the back stack.

Here is the full method for reference:

public void removeFragment(Fragment fragment, int transition, int transitionStyle) {
    if (DEBUG) Log.v(TAG, "remove: " + fragment + " nesting=" + fragment.mBackStackNesting);
    final boolean inactive = !fragment.isInBackStack();
    if (!fragment.mDetached || inactive) {
        if (mAdded != null) {
            mAdded.remove(fragment);
        }
        if (fragment.mHasMenu && fragment.mMenuVisible) {
            mNeedMenuInvalidate = true;
        }
        fragment.mAdded = false;
        fragment.mRemoving = true;
        moveToState(fragment, inactive ? Fragment.INITIALIZING : Fragment.CREATED,
                transition, transitionStyle, false);
    }
}

Possible answer to the question "How to know this fragment will never be used again"

To answer your question about how to know you can cancel your background tasks in a fragment, usually those fragments use setRetainInstance(true)

That way when the orientation of the device is changed the same Fragment will be reused and any ongoing background operations can be preserved.

When retain instance is true the fragment's onDestroy() method will not be called during orientation changes either so you can put your cancellation logic in there to know if the fragment is going away for good.


A better answer to how isRemoving works based on reviewing source code

Seeing as this answer has been accepted I feel I should fix a couple inaccuracies from my original answer. I said "This method is only ever called when a fragment is being removed that has been placed on the back stack" which is not entirely correct. Replacing a fragment also calls the method and correctly sets isRemoving to true as one example.

Now to answer your question on why isRemoving appears inconsistent across rotations by analyzing your log. My additional comments begin with ##

# Start Activity
# Click button in fragment 0 to add it to back stack and replace it with fragment 1
## FragmentManager.removeFragment is called on fragment 0 setting mRemoving to true
I/MainActivityFragment: 1: onCreate
# Rotate the device
I/MainActivityFragment: 0: onDestroy
I/MainActivityFragment: 0: isChangingConfigurations() == true
I/MainActivityFragment: 0: isRemoving() == true ## To emphasize, this is true because as soon as you replaced fragment 0 it was set to true in the FragmentManager.removeFragment method.
I/MainActivityFragment: 1: onDestroy
I/MainActivityFragment: 1: isChangingConfigurations() == true
I/MainActivityFragment: 1: isRemoving() == false ## fragment 1 is never actually removed so mRemoving is false.
I/MainActivityFragment: 0: onCreate
I/MainActivityFragment: 1: onCreate

# Rotate the device a second time
## after rotating the device the first time your same fragments are not reused but new instances are created. This resets all the internal state of the fragments so mRemoving is false for all fragments.
I/MainActivityFragment: 0: onDestroy
I/MainActivityFragment: 0: isChangingConfigurations() == true
I/MainActivityFragment: 0: isRemoving() == false # Correct result
I/MainActivityFragment: 1: onDestroy
I/MainActivityFragment: 1: isChangingConfigurations() == true
I/MainActivityFragment: 1: isRemoving() == false
I/MainActivityFragment: 0: onCreate
I/MainActivityFragment: 1: onCreate
# Click button in fragment 1 to add it to back stack and replace it with fragment 2
## fragment 1 now has mRemoving set to true in FragmentManager.removeFragment
I/MainActivityFragment: 2: onCreate
# Rotate the device
I/MainActivityFragment: 0: onDestroy
I/MainActivityFragment: 0: isChangingConfigurations() == true
I/MainActivityFragment: 0: isRemoving() == false ## still false from prior rotation
I/MainActivityFragment: 1: onDestroy
I/MainActivityFragment: 1: isChangingConfigurations() == true
I/MainActivityFragment: 1: isRemoving() == true ## true because mRemoving was set to true in FragmentManager.removeFragment.
I/MainActivityFragment: 2: onDestroy
I/MainActivityFragment: 2: isChangingConfigurations() == true
I/MainActivityFragment: 2: isRemoving() == false
I/MainActivityFragment: 0: onCreate
I/MainActivityFragment: 1: onCreate
I/MainActivityFragment: 2: onCreate

If you rotated the device again all fragments would return false from isRemoving().

Interestingly even if the same fragment instances were used you would still likely get the same output. There is a method in the Fragment class called initState that has the following comment:

Called by the fragment manager once this fragment has been removed, so that we don't have any left-over state if the application decides to re-use the instance. This only clears state that the framework internally manages, not things the application sets.

This method was called once for each fragment during rotation and one of the things it does is reset mRemoving to false.

4
votes

isRemoving is being called when a fragment is being replaced by another fragment using the call .replace not on configuration changes. I added a log:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.i(TAG, String.valueOf(getLevel()) + ": isAdded() == " +
            isAdded());
    Log.i(TAG, String.valueOf(getLevel()) + ": onCreate");
}

In summary:

# Start Activity 
**PORTRAIT**
I/MainActivityFragment: 0: isAdded() == true // **0.portrait Added**

# Click button in fragment 0 to add it to back stack and replace it with fragment 1
**REPLACE FRAGMENT**
I/MainActivityFragment: 1: isAdded() == true //  **1.portrait Added**

# Rotate the device 
**LANDSCAPE**
I/MainActivityFragment: 0: isRemoving() == true // replaced by 1 **0.portrait Removed**
I/MainActivityFragment: 1: isRemoving() == false // Not replaced in portrait
I/MainActivityFragment: 1: isAdded() == true //  **1.landscape Added**

# Rotate the device a second time 
**PORTRAIT**
I/MainActivityFragment: 1: isRemoving() == false  // Not being replaced in landscape
I/MainActivityFragment: 1: isAdded() == true // Display portrait again **1.portrait add** 

# Click button in fragment 1 to add it to back stack and replace it with fragment 2
**REPLACE FRAGMENT**
I/MainActivityFragment: 2: isAdded() == true **2.portrait Added**

# Rotate the device 
**LANDSCAPE**
I/MainActivityFragment: 1: isRemoving() == true 
// Is being replaced from previous replace fragment
I/MainActivityFragment: 2: isAdded() == true // Adding to landscape **1.landscape Added**

Full length logcat with added annotations

# Start Activity 
**PORTRAIT**

I/MainActivityFragment: 0: isAdded() == true // **0.portrait Added**
I/MainActivityFragment: 0: onCreate

# Click button in fragment 0 to add it to back stack and replace it with fragment 1
**REPLACE FRAGMENT**

I/MainActivityFragment: 1: isAdded() == true //  **1.portrait Added**
I/MainActivityFragment: 1: onCreate

# Rotate the device 
**LANDSCAPE**

I/MainActivity: Activity: onSaveInstanceState
I/MainActivityFragment: 0: onDestroy
I/MainActivityFragment: 0: isChangingConfigurations() == true
I/MainActivityFragment: 0: isRemoving() == true // replaced by 1 **0.portrait Removed**
I/MainActivityFragment: 0: isDetached() == false
I/MainActivityFragment: 1: onDestroy
I/MainActivityFragment: 1: isChangingConfigurations() == true
I/MainActivityFragment: 1: isRemoving() == false // Not replaced in portrait
I/MainActivityFragment: 1: isDetached() == false
I/MainActivity: Activity: onDestroy
I/MainActivityFragment: 0: isAdded() == false //  **nothing to do**
I/MainActivityFragment: 0: onCreate
I/MainActivityFragment: 1: isAdded() == true //  **1.landscape Added**
I/MainActivityFragment: 1: onCreate

# Rotate the device a second time 
**PORTRAIT**

I/MainActivityFragment: 0: onDestroy
I/MainActivityFragment: 0: isChangingConfigurations() == true
I/MainActivityFragment: 0: isRemoving() == false 
I/MainActivityFragment: 0: isDetached() == false
I/MainActivityFragment: 1: onDestroy
I/MainActivityFragment: 1: isChangingConfigurations() == true
I/MainActivityFragment: 1: isRemoving() == false  // Not being replaced in landscape
I/MainActivityFragment: 1: isDetached() == false
I/MainActivity: Activity: onDestroy
I/MainActivityFragment: 0: isAdded() == false
I/MainActivityFragment: 0: onCreate
I/MainActivityFragment: 1: isAdded() == true // Display **1.portrait add** 
I/MainActivityFragment: 1: onCreate

# Click button in fragment 1 to add it to back stack and replace it with fragment 2
**REPLACE FRAGMENT**

I/MainActivityFragment: 2: isAdded() == true **2.portrait Added**
I/MainActivityFragment: 2: onCreate

# Rotate the device 
**LANDSCAPE**

I/MainActivityFragment: 0: onDestroy
I/MainActivityFragme/nt: 0: isChangingConfigurations() == true
I/MainActivityFragment: 0: isRemoving() == false
I/MainActivityFragment: 0: isDetached() == false
I/MainActivityFragment: 1: onDestroy
I/MainActivityFragment: 1: isChangingConfigurations() == true
I/MainActivityFragment: 1: isRemoving() == true // Is being replaced
I/MainActivityFragment: 1: isDetached() == false
I/MainActivityFragment: 2: onDestroy
I/MainActivityFragment: 2: isChangingConfigurations() == true
I/MainActivityFragment: 2: isRemoving() == false 
I/MainActivityFragment: 2: isDetached() == false
I/MainActivity: Activity: onDestroy
I/MainActivityFragment: 0: isAdded() == false
I/MainActivityFragment: 0: onCreate
I/MainActivityFragment: 1: isAdded() == false
I/MainActivityFragment: 1: onCreate
I/MainActivityFragment: 2: isAdded() == true // Adding to landscape **1.landscape Added**
I/MainActivityFragment: 2: onCreate

As you can see when the fragment is replaced once, isremoving is only required once, the remainder of the logcat shows rotations only after that.

Seems the config change manages the fragment change differently to using replace.

//Start
I/MainActivityFragment: 0: isAdded() == true
I/MainActivityFragment: 0: onCreate
//Replace fragment
I/MainActivityFragment: 1: isAdded() == true
I/MainActivityFragment: 1: onCreate
I/MainActivity: Activity: onSaveInstanceState
I/MainActivityFragment: 0: onDestroy
I/MainActivityFragment: 0: isChangingConfigurations() == true
I/MainActivityFragment: 0: isRemoving() == true
I/MainActivityFragment: 0: isDetached() == false
I/MainActivityFragment: 1: onDestroy
I/MainActivityFragment: 1: isChangingConfigurations() == true
I/MainActivityFragment: 1: isRemoving() == false
I/MainActivityFragment: 1: isDetached() == false
I/MainActivity: Activity: onDestroy
I/MainActivityFragment: 0: isAdded() == false
I/MainActivityFragment: 0: onCreate
I/MainActivityFragment: 1: isAdded() == true
I/MainActivityFragment: 1: onCreate
I/MainActivity: Activity: onSaveInstanceState
I/MainActivityFragment: 0: onDestroy
I/MainActivityFragment: 0: isChangingConfigurations() == true
I/MainActivityFragment: 0: isRemoving() == false
I/MainActivityFragment: 0: isDetached() == false
I/MainActivityFragment: 1: onDestroy
I/MainActivityFragment: 1: isChangingConfigurations() == true
I/MainActivityFragment: 1: isRemoving() == false
I/MainActivityFragment: 1: isDetached() == false
I/MainActivity: Activity: onDestroy
I/MainActivityFragment: 0: isAdded() == false
I/MainActivityFragment: 0: onCreate
I/MainActivityFragment: 1: isAdded() == true
I/MainActivityFragment: 1: onCreate
I/MainActivity: Activity: onSaveInstanceState
I/MainActivityFragment: 0: onDestroy
I/MainActivityFragment: 0: isChangingConfigurations() == true
I/MainActivityFragment: 0: isRemoving() == false
I/MainActivityFragment: 0: isDetached() == false
I/MainActivityFragment: 1: onDestroy
I/MainActivityFragment: 1: isChangingConfigurations() == true
I/MainActivityFragment: 1: isRemoving() == false
I/MainActivityFragment: 1: isDetached() == false
I/MainActivity: Activity: onDestroy
I/MainActivityFragment: 0: isAdded() == false
I/MainActivityFragment: 0: onCreate
I/MainActivityFragment: 1: isAdded() == true
I/MainActivityFragment: 1: onCreate
I/MainActivity: Activity: onSaveInstanceState
I/MainActivityFragment: 0: onDestroy
I/MainActivityFragment: 0: isChangingConfigurations() == true
I/MainActivityFragment: 0: isRemoving() == false
I/MainActivityFragment: 0: isDetached() == false
I/MainActivityFragment: 1: onDestroy
I/MainActivityFragment: 1: isChangingConfigurations() == true
I/MainActivityFragment: 1: isRemoving() == false
I/MainActivityFragment: 1: isDetached() == false
I/MainActivity: Activity: onDestroy
I/MainActivityFragment: 0: isAdded() == false
I/MainActivityFragment: 0: onCreate
I/MainActivityFragment: 1: isAdded() == true
I/MainActivityFragment: 1: onCreate

I'm a bit unclear on your actual question on what you are trying to stop using when a fragment is gone. Whenever the fragment is replaced or on config change you can dispose of that fragment and anything associated with it, if that is your wish. If you see these results, you'll see the other fragments are still in the background, so what you decide to do with them is your choice.

3
votes

isRemoving() returns mRemoving which, from the code comment means:

If set this fragment is being removed from its activity.

This is mainly set in FragmentManager.removeFragment()

Also note that:

  • This behaviour can change accross implementations, particularly framework vs AppCompat
  • Fragment Transactions are asynchronous, its possible that the value returned by isRemoving() changes when you reproduce your experiment

I don't know what you want to do with this information. If you want to know if the fragment is active, you can use:

isAdded() && !isRemoving() && !isDetached()

Edit: You now ask how to know that an instance of a fragment should stop asynchronous work (because the fragment is being removed). I'd do this with:

getActivity().isFinishing() || isRemoving() || isDetached()
1
votes

The only idea I can say: isRemoving() method returns the inner parameter mRemoving that means 'the removing is in progress'. Usually, it means there is a manager that, for example, releases memory in another thread. That's why from time to time you will receive the different values. This is obvoiusly not a callback. Just a simple state.

1
votes

Possible flow:

Application starts. On creating Fragment 0, mRemoving is initialized to false. On replacing Fragment 0 with Fragment 1, mRemoving is set to true for Fragment 0 by removeFragment().

Configuration changes. During configuration change, that field (mRemoving in Fragment 0) is read to be true as it was set by removeFragment() when we first replaced Fragment 0.

But when configuration change occurs, FragmentManager possibly handles the flow differently as opposed to our manual replace() transaction. And, as we know when adding Fragments mRemoving is initialized to false, and considering removeFragment() doesn't get called, mRemoving is false the second time.