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:
ACTIVITY_CREATED
) vs. mState=1 (CREATED
)The differences between the second (first time destroyed) and third (second+ time destoyed) are:
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).
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With