Why won't Fragment retain state when screen is rotated?
D

2

29

I've been having some trouble getting some custom DialogPreference subclasses inside a PreferenceFragment to remain visible when the screen is rotated. I don't experience this problem when using a PreferenceActivity, so I don't know whether it's an Android bug or a problem with my code, but I'd like someone to confirm whether they are having the same experience.

To test this, first create a preference screen containing at least one DialogPreference (it doesn't matter which subclass). Then display it in a PreferenceActivity. When you run your app, press on the DialogPreference so that it's dialog shows. Then rotate the screen so the orientation changes. Does the dialog remain visible?

Then try the same, but with a PreferenceFragment to display your preferences instead of a PreferenceActivity. Again, does the dialog remain visible when you rotate the screen?

So far, I've found that the dialog will remain visible if using a PreferenceActivity, but not if using a PreferenceFragment. Looking at the source code for DialogPreference, it seems that the correct behaviour is for the dialog to remain visible, because isDialogShowing is the state information that gets saved when onSaveInstanceState() is called on screen re-orientation. Therefore, I think a bug may be preventing the PreferenceFragment (and everything inside it) from restoring that state information.

If it is an Android bug, then it has far-reaching implications, because anyone using PreferenceFragment cannot save and restore state information.

Can someone please confirm? If it's not a bug, then what is going on?

Diatropism answered 6/1, 2013 at 16:32 Comment(0)
D
56

Finally figured out a solution to this problem. Turns out it's not a bug, but a problem/oversight in the Android developer documentation.

You see, I was following the PreferenceFragment tutorial here. That article tells you to do the following in order to instantiate your PreferenceFragment within an Activity:

public class SettingsActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Display the fragment as the main content.
        getFragmentManager().beginTransaction()
                .replace(android.R.id.content, new SettingsFragment())
                .commit();
    }
} 

The problem with this is that when you change the screen orientation (or any other action that destroys and re-creates the Activity), your PreferenceFragment will get created twice, which is what causes it to lose its state.

The first creation will occur via the Activity's call to super.onCreate() (shown above), which will call the onActivityCreated() method for your PreferenceFragment () and the onRestoreInstanceState() method for each Preference it contains. These will successfully restore the state of everything.

But then once that call to super.onCreate() returns, you can see that the onCreate() method will then go on to create the PreferenceFragment a second time. Because it is pointlessly created again (and this time, without state information!), all of the state that was just successfully restored will be completely discarded/lost. This explains why a DialogPreference that may be showing at the time that the Activity is destroyed will no longer be visible once the Activity is re-created.

So what's the solution? Well, just add a small check to determine whether the PreferenceFragment has already been created, like so:

public class SettingsActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Fragment existingFragment = getFragmentManager().findFragmentById(android.R.id.content);
        if (existingFragment == null || !existingFragment.getClass().equals(SettingsFragment.class))
        {
            // Display the fragment as the main content.
            getFragmentManager().beginTransaction()
                .replace(android.R.id.content, new SettingsFragment())
                .commit();
        }
    }
}

Or another way is to simply check if onCreate() is meant to restore state or not, like so:

public class SettingsActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (savedInstanceState == null)
        {
            // Display the fragment as the main content.
            getFragmentManager().beginTransaction()
                .replace(android.R.id.content, new SettingsFragment())
                .commit();
        }
    }
}

So I guess the lesson learnt here is that onCreate() has a dual role - it can set up an Activity for the first time, or it can restore from an earlier state.

The answer here led me to realizing this solution.

Diatropism answered 13/1, 2013 at 13:36 Comment(6)
The third option is the standard way of checking for the "first" onCreate executionScuffle
if(savedInstanceState == null){} that statement will only resolve to true on the first creation of an ActivityScuffle
Isn't that what I have above...?Rim
yes, I was just clarifying since you have three different ways of identifying the first run of an onCreate()Scuffle
+1 - demo I followed also created a new Fragment() in onCreate(). But this is a tricky one to track down. Nice explanation.Adin
I don't know why, but the first method with checking if the fragment already exists did not work for me. Only the checking if (savedInstanceState == null) worked.Pittsburgh
T
0

I have indeed had this issue myself. There is a bug where the DialogFragment doesn't restore state because it is null, or at least it happened to me.

Using multiple sources I eventually got a solution working. Have your dialog extend this BaseDialogFragment:

import android.app.Dialog;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.support.v4.app.DialogFragment;

import com.actionbarsherlock.app.SherlockDialogFragment;

public class BaseDialogFragment extends DialogFragment {

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        if (savedInstanceState == null || savedInstanceState.isEmpty())
            savedInstanceState = WorkaroundSavedState.savedInstanceState;

        setRetainInstance(true);
        Log.d("TAG", "saved instance state oncreate: "
                + WorkaroundSavedState.savedInstanceState);
        super.onCreate(savedInstanceState);
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState)
    {
        if (savedInstanceState == null || savedInstanceState.isEmpty())
            savedInstanceState = WorkaroundSavedState.savedInstanceState;
        Log.d("TAG", "saved instance state oncretaedialog: "
                + WorkaroundSavedState.savedInstanceState);

        return super.onCreateDialog(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        if (savedInstanceState == null || savedInstanceState.isEmpty())
            savedInstanceState = WorkaroundSavedState.savedInstanceState;

        Log.d("TAG", "saved instance state oncretaeview: "
                + WorkaroundSavedState.savedInstanceState);

        return super.onCreateView(inflater, container, savedInstanceState);
    }

    @Override
    public void onDestroyView() // necessary for restoring the dialog
    {
        if (getDialog() != null && getRetainInstance())
            getDialog().setOnDismissListener(null);

        super.onDestroyView();
    }

    @Override
    public void onSaveInstanceState(Bundle outState)
    {
        // ...

        super.onSaveInstanceState(outState);
        WorkaroundSavedState.savedInstanceState = outState;
        Log.d("TAG", "saved instance state onsaveins: "
                + WorkaroundSavedState.savedInstanceState);

    }

    @Override
    public void onDestroy()
    {
        WorkaroundSavedState.savedInstanceState = null;
        super.onDestroy();
    }

    /**
     * Static class that stores the state of the task across orientation
     * changes. There is a bug in the compatibility library, at least as of the
     * 4th revision, that causes the save state to be null in the dialog's
     * onRestoreInstanceState.
     */
    public static final class WorkaroundSavedState {
        public static Bundle savedInstanceState;
    }
}

Note that in any subclasses whose methods have a savedInstanceState parameter, you may have to call super with WorkaroundSavedState.savedInstanceState. And when you're restoring state (i.e. in onCreate(), just ignore the savedInstanceState and instead use WorkaroundSavedState.savedInstanceState. The static holder isn't the cleanest solution, but it works. Just make sure to set it to null in your onDestroy().

In any case, my DialogFragment does not disappear when I rotate the screen (and that's without any configChanges). Let me know if this code addresses your issue and if not I'll take a look at what's going on. Also note that I haven't tested this within PreferenceFragment but instead other Fragments from the compatibility class or from ActionBarSherlock.

Tabernacle answered 10/1, 2013 at 5:37 Comment(5)
Why introduce a third-party dependency (ActionBarSherlock)?Rim
You don't need to. Just have it extend DialogFragment insteadTabernacle
I just edited my answer. Personally I was just using SherlockDialogFragment because I was using ABS, but my answer should work without ABS too. Try it out now by having your own DialogFragments extend this class.Tabernacle
Figured out a solution. See my answer.Rim
Oh I see; we were likely having different issues as I did not redisplay the fragment in onCreate.Tabernacle

© 2022 - 2024 — McMap. All rights reserved.