Fragment recreated every time after orientation ch

2019-04-24 14:33发布

问题:

Update: It turns out that the problem is from elsewhere. Thanks @Luksprog for pointing out what I overlooked.

  1. The project is created using Android Studio's navigation drawer pattern. The drawer is implemented in the NavigationDrawerFragment class.
  2. The fragment holding the view pager is added when a particular item in the drawer is selected. The code is implemented my home activity.
  3. When screen rotates, the onCreate() method of NavigationDrawerFragment is called, preserving last selected item.
  4. And here is what went wrong - upon recreation, NavigationDrawerFragment will call selectItem() again, which triggers my menu item selected handler. This causes the ListFragment restored by Android.

This can be prevented by checking the active menu item in my menu selection handler code.


I want to retain the last viewing page index of the ViewPager when the activity is recreated by whatever reason, e.g. orientation change.

The ViewPager is in a Fragment (named ListFragment), which is attached to an activity. I am using the compat library, so the fragment is a subclass of android.support.v4.app.Fragment.

I thought that it could be done by overriding the onSaveInstanceState() method and add appropriate logic in onCreate(), as metioned in the doc:

To properly handle a restart, it is important that your activity restores its previous state through the normal Activity lifecycle, in which Android calls onSaveInstanceState() before it destroys your activity so that you can save data about the application state. You can then restore the state during onCreate() or onRestoreInstanceState().

But the situation seems different for fragments. The page index can be correctly restored when I navigate from this ListFragment to another activity and pressed "back". However when I rotate my device, the page index is lost.

I added some logging to see what's wrong. From the log I found that although onSaveInstanceState() of the ListFragment(I'll call it ListFragment A) is called properly, this particular Fragment class is no longer shown in the activity. When the orientation changed and the activity is recreated, Android calls onSaveInstanceState() followed by onDetach() to detach this fragment. Then Android creates a new instance of ListFragment (I'll call it ListFragment B) and attach it to the new, rotated activity. This ListFragment B has an empty savedInstanceState passed to the constructor, and thus the last page index (and any configuration in savedInstanceState of Fragment A) is lost.

In fact, a new instance of ListFragment will be created every time a screen rotate occurs, but it seems that the old ones will not be destroyed. I see logs like below when I rotate the device:

D/ListFragment﹕ [1110257048] onSaveInstanceState() called, storing last page index 3
D/ListFragment﹕ [1109835992] onSaveInstanceState() called, storing last page index 0
D/ListFragment﹕ [1108826176] onSaveInstanceState() called, storing last page index 0
D/ListFragment﹕ [1108083096] onSaveInstanceState() called, storing last page index 0
D/ListFragment﹕ [1106541040] onSaveInstanceState() called, storing last page index 0
D/ListFragment﹕ [1108316656] onSaveInstanceState() called, storing last page index 0
D/ListFragment﹕ [1109134136] onSaveInstanceState() called, storing last page index 0
D/ListFragment﹕ [1108630992] onSaveInstanceState() called, storing last page index 0
D/ListFragment﹕ [1108592888] onSaveInstanceState() called, storing last page index 0
D/ListFragment﹕ [1109729064] onSaveInstanceState() called, storing last page index 0
D/ListFragment﹕ [1110257048] onDestroy()
D/ListFragment﹕ [1110257048] onDetach()
D/ListFragment﹕ [1109835992] onDestroy()
D/ListFragment﹕ [1109835992] onDetach()
D/ListFragment﹕ [1108826176] onDestroy()
D/ListFragment﹕ [1108826176] onDetach()
D/ListFragment﹕ [1108083096] onDestroy()
D/ListFragment﹕ [1108083096] onDetach()
D/ListFragment﹕ [1106541040] onDestroy()
D/ListFragment﹕ [1106541040] onDetach()
D/ListFragment﹕ [1108316656] onDestroy()
D/ListFragment﹕ [1108316656] onDetach()
D/ListFragment﹕ [1109134136] onDestroy()
D/ListFragment﹕ [1109134136] onDetach()
D/ListFragment﹕ [1108630992] onDestroy()
D/ListFragment﹕ [1108630992] onDetach()
D/ListFragment﹕ [1108592888] onDestroy()
D/ListFragment﹕ [1108592888] onDetach()
D/ListFragment﹕ [1109729064] onDestroy()
D/ListFragment﹕ [1109729064] onDetach()
D/ListFragment﹕ [1110903656] onAttach()
D/ListFragment﹕ [1110903656] onCreate()
D/ListFragment﹕ [1110903656] savedInstanceState is not NULL.
D/ListFragment﹕ [1110903656] Retrieving last page index 3
D/ListFragment﹕ [1110905248] onAttach()
D/ListFragment﹕ [1110905248] onCreate()
D/ListFragment﹕ [1110905248]   savedInstanceState is not NULL.
D/ListFragment﹕ [1110905248]   Retrieving last page index 0
D/ListFragment﹕ [1110906440] onAttach()
D/ListFragment﹕ [1110906440] onCreate()
D/ListFragment﹕ [1110906440]   savedInstanceState is not NULL.
D/ListFragment﹕ [1110906440]   Retrieving last page index 0
D/ListFragment﹕ [1110907632] onAttach()
D/ListFragment﹕ [1110907632] onCreate()
D/ListFragment﹕ [1110907632]   savedInstanceState is not NULL.
D/ListFragment﹕ [1110907632]   Retrieving last page index 0
D/ListFragment﹕ [1110908824] onAttach()
D/ListFragment﹕ [1110908824] onCreate()
D/ListFragment﹕ [1110908824]   savedInstanceState is not NULL.
D/ListFragment﹕ [1110908824]   Retrieving last page index 0
D/ListFragment﹕ [1110910016] onAttach()
D/ListFragment﹕ [1110910016] onCreate()
D/ListFragment﹕ [1110910016]   savedInstanceState is not NULL.
D/ListFragment﹕ [1110910016]   Retrieving last page index 0
D/ListFragment﹕ [1110911208] onAttach()
D/ListFragment﹕ [1110911208] onCreate()
D/ListFragment﹕ [1110911208]   savedInstanceState is not NULL.
D/ListFragment﹕ [1110911208]   Retrieving last page index 0
D/ListFragment﹕ [1110912400] onAttach()
D/ListFragment﹕ [1110912400] onCreate()
D/ListFragment﹕ [1110912400]   savedInstanceState is not NULL.
D/ListFragment﹕ [1110912400]   Retrieving last page index 0
D/ListFragment﹕ [1110913592] onAttach()
D/ListFragment﹕ [1110913592] onCreate()
D/ListFragment﹕ [1110913592]   savedInstanceState is not NULL.
D/ListFragment﹕ [1110913592]   Retrieving last page index 0
D/ListFragment﹕ [1110914784] onAttach()
D/ListFragment﹕ [1110914784] onCreate()
D/ListFragment﹕ [1110914784]   savedInstanceState is not NULL.
D/ListFragment﹕ [1110914784]   Retrieving last page index 0
D/HomeActivity﹕ fragment updated
D/ListFragment﹕ [1110914784] onCreateView()
D/ListFragment﹕ [1111031048] onAttach()
D/HomeActivity﹕ Fragment attached.
D/ListFragment﹕ [1111031048] onCreate()
D/ListFragment﹕ [1111031048]   savedInstanceState is NULL.
D/ListFragment﹕ [1111031048] onCreateView()
D/ListFragment﹕ [1111031048] onResume(), restoring page index 0

This is the log after I rotated the screen for about 10 times. The number in the tag is the classes' hashCode(). Above lines shows that onSaveInstanceState() and onCreate() of the previously created fragments still get called even after they are replaced by the latest (1111031048) one.

Note that I didn't call setRetainInstance() in the fragment class. In fact, I tried both setRetainInstance(false) and setRetainInstance(true) but it doesn't change anything.

Did I do anything wrong here? I can understand that ListFragment needs to be recreated, but why savedInstanceState is null? And if this is the expected behavior, what is the correct way to solve my need, i.e. keeping the page index when configuration changes?

It should be possible to make the page index a static class variable, but I'm not sure if it is actually solving the issue, or just hiding it (because I smell memory leak in the log above).

回答1:

Even though the answer has already been accepted, let me clarify this more: the "issue" is in the Android Studio template. The problem, as pointed out by Edmund, is in the Navigation Drawer calling the menu when recreated, thus re-calling the Fragment. To solve this, I made this slight modification on the NavigationDrawerFragment.java file as proposed by Android Studio. Original:

    @Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Read in the flag indicating whether or not the user has demonstrated awareness of the
    // drawer. See PREF_USER_LEARNED_DRAWER for details.
    SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity());
    mUserLearnedDrawer = sp.getBoolean(PREF_USER_LEARNED_DRAWER, false);

    if (savedInstanceState != null) {
        mCurrentSelectedPosition = savedInstanceState.getInt(STATE_SELECTED_POSITION);
        mFromSavedInstanceState = true;
    }

    // Select either the default item (0) or the last selected item.
    selectItem(mCurrentSelectedPosition);

}

New one:

    @Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Read in the flag indicating whether or not the user has demonstrated awareness of the
    // drawer. See PREF_USER_LEARNED_DRAWER for details.
    SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity());
    mUserLearnedDrawer = sp.getBoolean(PREF_USER_LEARNED_DRAWER, false);

    if (savedInstanceState != null) {
        mCurrentSelectedPosition = savedInstanceState.getInt(STATE_SELECTED_POSITION);
        mFromSavedInstanceState = true;
    } else {
        // Select either the default item (0) or the last selected item.
        selectItem(mCurrentSelectedPosition);
    }
}

Just lost half a production day to figure this out. Hope it may help somebody else.



回答2:

As updated in the question, this is resolved and thanks again to @Luksprog for pointing out what I overlooked.

The behavior of Fragment actually aligns with Activity classes.

Here is the cause of my issue:

  • The project is created using navigation drawer pattern provided by Android Studio's "create project" wizard. The drawer is implemented in the NavigationDrawerFragment class.
  • The fragment holding the view pager is added when a particular item in the drawer is selected. The code is implemented my home activity.
  • When screen rotates, the onCreate() method of NavigationDrawerFragment is called, preserving last selected item.
  • And here is what went wrong - upon recreation, NavigationDrawerFragment will call selectItem() again, which triggers my menu item selected handler. This causes the ListFragment be replaced.

This can be prevented by checking the active menu item in my menu selection handler code, or by disabling that selectItem() call.



回答3:

You dont need to handle on savedInstanceState. Because all fragments stored in fragment manger (it like array of fragments) and when the orientation changes activity destroys but fragment manager still hold all fragments that was added before. So you just need to check if fragment was already added.

private void openFragment(Fragment fragment, String tag) {
            FragmentManager fragmentManager = getSupportFragmentManager();
            FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
            Fragment existingFragment = fragmentManager.findFragmentByTag(tag);
            if (existingFragment != null) {
                fragmentTransaction.add(R.id.container, fragment, tag);
                fragmentTransaction.commit();
            }
        }


回答4:

Try using onCreateView() here instead of onCreate(), which is specific to Fragments and might make a difference.

Override onSaveInstanceState() to store your state, and recover it in onCreateView() using savedInstanceState.get*(). This is how we do it in our apps, and it should work. As @Ari mentioned, make sure to call super.onSaveInstanceState(outState).

class MyFragment extends ListFragment {

    @Override
    public void onSaveInstanceState(Bundle outState) {
        outState.putString(ARG_PAGE, page); 
        super.onSaveInstanceState(outState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        if(savedInstanceState != null){
            if(savedInstanceState.containsKey(ARG_PAGE)){
                page = savedInstanceState.getInteger(ARG_PAGE);
            }
        }
    }
}


回答5:

In AndroidManifest.xml add

<activity android:name=".MainActivity"
android:configChanges="orientation|screenSize">
</activity>