I have an app with a navigation drawer. Everything was peachy until I rotated the screen, and noticed that the views are not preserving their states. Digging deeper I realized onCreateView for my fragment was called twice, the second time without saved state. What did I do wrong?
Logging
To isolate the problem, I created another app, generated the activity from the Android Studio template, and just added a single logging line:
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
Log.i("sqisland", "onCreateView. Saved state? "
+ (savedInstanceState != null));
View rootView = inflater.inflate(
R.layout.fragment_main, container, false);
return rootView;
}
And indeed, onCreateView called twice on rotation, once with saved state and once without:
I/sqisland(24201): onCreateView. Saved state? true
I/sqisland(24201): onCreateView. Saved state? false
More logging
I enabled debugging on the FragmentManager to get more information:
FragmentManager.enableDebugLogging(true);
Time to read the tea leaves, and there are lots of it. I picked out the relevant part:
Saved state of NavigationDrawerFragment
{41ea1a38 #0 id=0x7f07003e}
Saved state of PlaceholderFragment
{41f027a8 #1 id=0x7f07003d}
Freeing fragment index NavigationDrawerFragment
{41ea1a38 #0 id=0x7f07003e}
Freeing fragment index PlaceholderFragment
{41f027a8 #1 id=0x7f07003d}
On rotation, the FragmentManager saves the states of the NavigationDrawerFragment and PlaceholderFragment, then frees them.
Instantiated fragment NavigationDrawerFragment
{41f90658 #0 id=0x7f07003e}
restoreAllState: active #0: NavigationDrawerFragment
{41f90658 #0 id=0x7f07003e}
Instantiated fragment PlaceholderFragment
{41f91018 #1 id=0x7f07003d}
restoreAllState: active #1: PlaceholderFragment
{41f91018 #1 id=0x7f07003d}
Then it instantiates new fragments for the new activity, and restores the saved states.
moveto CREATED: PlaceholderFragment
{41f91018 #1 id=0x7f07003d}
moveto CREATED: NavigationDrawerFragment
{41f90658 #0 id=0x7f07003e}
Commit: BackStackEntry{41ed3088}
mName=null mIndex=-1 mCommitted=false
Operations:
Op #0: REPLACE PlaceholderFragment
{41f849c8 id=0x7f07003d}
Next it moves the fragments to onCreate. There is a commit because onCreate in NavigationDrawerFragment calls selectItem, which triggers the activity to instantiate a new PlaceholderFragment.
@Override
public void onNavigationDrawerItemSelected(int position) {
// update the main content by replacing fragments
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.container,
PlaceholderFragment.newInstance(position + 1))
.commit();
}
Run: BackStackEntry{41ed3088}
OP_REPLACE: adding=PlaceholderFragment
{41f849c8 id=0x7f07003d}
old=NavigationDrawerFragment
{41f90658 #0 id=0x7f07003e}
Now the FragmentManager runs the transcation, looking for fragment id 0x7f07003d for replacement, which is R.id.container.
It skipped the NavigationDrawerFragment because the id does not match.
OP_REPLACE: adding=PlaceholderFragment
{41f849c8 id=0x7f07003d}
old=PlaceholderFragment
{41f91018 #1 id=0x7f07003d}
Next it found the PlaceholderFragment which it restored from rotation, with id 0x7f07003d. Bingo! This is the fragment to replace.
remove: PlaceholderFragment
{41f91018 #1 id=0x7f07003d} nesting=0
Freeing fragment index PlaceholderFragment
{41f91018 #1 id=0x7f07003d}
add: PlaceholderFragment
{41f849c8 id=0x7f07003d}
Allocated fragment index PlaceholderFragment
{41f849c8 #1 id=0x7f07003d}
moveto CREATED: PlaceholderFragment
{41f849c8 #1 id=0x7f07003d}
Replacing a fragment means removing it and adding it back. Afterwards, the FragmentManager moves the fragment into onCreate.
This is why I see onCreate called twice, once with saved state, and once without.
The fix
A quick summary:
- Device was rotated.
FragmentManager saves state for all fragments, then frees them.
- New activity. New fragments get instantiated, with states restored.
- Fragments get moved into
onCreate.
onCreate of NavigationDrawerFragment triggers a replace transaction.
- The replace transaction removes the restored
PlaceholderFragment with a new one, which has no saved state.
To prevent the second PlaceholderFragment from being created, we need to tell NavigationDrawerFragment not to trigger a replace transaction when it is restored from saved state. I added a parameter to selectItem:
private void selectItem(
int position, boolean fromSavedInstanceState) {
mCurrentSelectedPosition = position;
if (mDrawerListView != null) {
mDrawerListView.setItemChecked(position, true);
}
if (mDrawerLayout != null) {
mDrawerLayout.closeDrawer(mFragmentContainerView);
}
if (mCallbacks != null) {
mCallbacks.onNavigationDrawerItemSelected(
position, fromSavedInstanceState);
}
}
When the selectItem is called from onCreate, fromSavedInstanceState = (savedInstanceState != null). When it is called from the ListView OnItemClickListener, it is false.
The activity only commit the replace transaction if needed:
@Override
public void onNavigationDrawerItemSelected(
int position, boolean fromSavedInstanceState) {
if (!fromSavedInstanceState) {
// update the main content by replacing fragments
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.container,
PlaceholderFragment.newInstance(position + 1))
.commit();
}
}
Don't forget to change the signature of the interface:
public static interface NavigationDrawerCallbacks {
void onNavigationDrawerItemSelected(
int position, boolean fromSavedInstanceState);
}
Done, right? Not so fast. This indeed removed the second creation of PlaceholderFragment, but the action bar title is not updated. Here is why:
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
((MainActivity) activity).onSectionAttached(
getArguments().getInt(ARG_SECTION_NUMBER));
}
We are updating the title onAttach, but when the fragment is restored by the FragmentManager, it is attached before activity onCreate is called, so the title gets overwritten by the app title.
Instead, we want to update the title after activity onCreate is called.
Time to consult the fragment lifecycle:
-
onAttach(Activity) called once the fragment is associated with its activity.
-
onCreate(Bundle) called to do initial creation of the fragment.
-
onCreateView(LayoutInflater, ViewGroup, Bundle) creates and returns the view hierarchy associated
with the fragment.
-
onActivityCreated(Bundle) tells the fragment that its activity has
completed its own Activity.onCreate().
-
onViewStateRestored(Bundle) tells the fragment that all of the saved
state of its view hierarchy has been restored.
-
onStart() makes the fragment visible to the user (based on its
containing activity being started).
-
onResume() makes the fragment interacting with the user (based on its
containing activity being resumed).
onActivityCreated it is:
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
((MainActivity) getActivity()).updateTitle(
getArguments().getInt(ARG_SECTION_NUMBER));
}
I renamed onSectionAttached to updateTitle as well:
public void updateTitle(int number) {
switch (number) {
case 1:
mTitle = getString(R.string.title_section1);
break;
case 2:
mTitle = getString(R.string.title_section2);
break;
case 3:
mTitle = getString(R.string.title_section3);
break;
}
}
Yay, everything works!
Did you read all the tea leaves with me? Wow. Go get yourself a nice drink. You deserve it.