Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fragment back stack and isRemoving()

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).

like image 379
imgx64 Avatar asked Jan 07 '16 07:01

imgx64


1 Answers

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.

like image 130
George Mulligan Avatar answered Sep 28 '22 10:09

George Mulligan