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.