Fragments, DialogFragment, and Screen Rotation

2019-01-12 23:55发布

问题:

I have an Activity that calls setContentView with this XML:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal"
    >
    <fragment android:name="org.vt.indiatab.GroupFragment"
        android:id="@+id/home_groups"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_weight="1" />
            <..some other fragments ...>
</LinearLayout>

The GroupFragment extends Fragment, and all is well there. However, I show a DialogFragment from within GroupFragment. This shows correctly, HOWEVER when the screen rotates, I get a Force Close.

What's the proper way to display a DialogFragment from within another Fragment other than DialogFragment.show(FragmentManager, String)?

回答1:

There's a bug in the compatibility library that can cause this. Try putting this in you dialogfragment:

@Override
public void onDestroyView() {
  if (getDialog() != null && getRetainInstance())
    getDialog().setOnDismissListener(null);
  super.onDestroyView();
}

I also suggest setting your dialogfragment as retained, so it won't get dismissed after the rotation. Put "setRetainInstance(true);" e.g. in the onCreate() method.



回答2:

OK, while Zsombor's method works, this is due to me being inexperienced with Fragments and his solution causes issues with the saveInstanceState Bundle.

Apparently (at least for a DialogFragment), it should be a public static class. You also MUST write your own static DialogFragment newInstance() method. This is because the Fragment class calls the newInstance method in its instantiate() method.

So in conclusion, you MUST write your DialogFragments like so:

public static class MyDialogFragment extends DialogFragment {

    static MyDialogFragment newInstance() {
        MyDialogFragment d = new MyDialogFragment();
        return d;
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        ...
    }
}

And show them with:

private void showMyDialog() {
    MyDialogFragment d = MyDialogFragment.newInstance();
    d.show(getFragmentManager(), "dialog");
}

This may be unique to the ActionBarSherlock Library, but the official samples in the SDK documentation use this paradigm also.



回答3:

To overcome the Bundle always being null, I save it to a static field in onSaveInstanceState. It's a code smell, but the only solution I found for both restoring the dialog and saving the state.

The Bundle reference should be nulled in onDestroy.

@Override
public void onCreate(Bundle savedInstanceState)
{
    if (savedInstanceState == null)
        savedInstanceState = HackishSavedState.savedInstanceState;

    setRetainInstance(true);
}

@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
    if (savedInstanceState == null)
        savedInstanceState = HackishSavedState.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)
{
    ...

    HackishSavedState.savedInstanceState = outState;
    super.onSaveInstanceState(outState);
}

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

private static class HackishSavedState
{
    static Bundle savedInstanceState;
}


回答4:

I used a mix of the presented solutions and added one more thing. This is my final solution:

I used setRetainInstance(true) in the onCreateDialog; I used this:

public void onDestroyView() {
    if (getDialog() != null && getRetainInstance())
        getDialog().setDismissMessage(null);
    super.onDestroyView();
}

And as a workaround of the savedInstanceState not working, I created a private class called StateHolder (the same way a holder is create for a listView):

private class StateHolder {
    String name;
    String quantity;
}

I save the state this way:

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
    super.onSaveInstanceState(savedInstanceState);
    stateHolder = new StateHolder();
    stateHolder.name = actvProductName.getText().toString();
    stateHolder.quantity = etProductQuantity.getText().toString();
}

In the onDismiss method I set the stateHolder back to null. When the dialog is created, it verifies if the stateHolder isn't null to recover the state or just initialize everything normally.



回答5:

I solved this issue with answers of @ZsomborErdődy-Nagy and @AndyDennie . You must subclass this class and in you parent fragment call setRetainInstance(true), and dialogFragment.show(getFragmentManager(), "Dialog");

 public class AbstractDialogFragment extends DialogFragment {

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

        @Override
        public void onDestroyView() {
            if (getDialog() != null && getRetainInstance())
                getDialog().setDismissMessage(null);
            super.onDestroyView();
        }
    }


回答6:

I had a similar issue, however none of the above worked for me. In the end I needed to create the fragment in code instead of in the XML layout.

See: Replacing fragments and orientation change



回答7:

I ran into this on my project and none of the above solutions helped.

If the exception looks something like


java.lang.RuntimeException: Unable to start activity ComponentInfo{ 

...

        Caused by: java.lang.IllegalStateException: Fragment.... 
        did not create a view.

It's caused by an issue with a fallback container Id that gets used after rotation. See this ticket for more details:

https://code.google.com/p/android/issues/detail?id=18529

Basically you can prevent the crash by making sure all of your xml fragments have a tag defined in the layout. This prevents the fallback condition from occurring if you rotate when a fragment is visible.

In my case I was able to apply this fix without having to override onDestroyView() or setRetainInstance(true), which is the common recommendation for this situation.



回答8:

I encountered this problem and the onDestroyView() trick wasn't working. It turned out that it was because I was doing some rather intensive dialog creation in onCreate(). This included saving a reference to the AlertDialog, which I would then return in onCreateDialog().

When I moved all of this code to onCreateDialog() and stopped retaining a reference to the dialog, it started working again. I expect I was violating one of the invariants DialogFragment has about managing its dialog.



回答9:

In onCreate() call setRetainInstance(true) and then include this:

@Override
public void onDestroyView() {
    if (getDialog() != null && getRetainInstance()) {
        getDialog().setOnDismissMessage(null);
    }
    super.onDestroyView();
}

When you call setRetainInstance(true) in onCreate(), onCreate() will no longer be called across orientation changes, but onCreateView() will still be called.

So you can still save the state to your bundle in onSaveInstanceState() and then retrieve it in onCreateView():

@Override
public void onSaveInstanceState(Bundle outState) {

    super.onSaveInstanceState(outState);

    outState.putInt("myInt", myInt);
}

@Override
public View onCreateView(LayoutInflater inflater, 
                         ViewGroup container, Bundle savedInstanceState) {

    View view = inflater.inflate(R.layout.my_layout, container);

    if (savedInstanceState != null) {

        myInt = savedInstanceState.getInt("myInt");
    }

    ...

    return view;
}