碎片、DialogFragment和屏幕旋转

25

我有一个Activity,它通过这个XML调用了setContentView:

<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>

GroupFragment继承自Fragment,一切都很好。然而,我在GroupFragment中从一个DialogFragment中显示一个对话框。这个对话框可以正确地显示,但是当屏幕旋转时,我会得到一个强制关闭的错误。

除了使用DialogFragment.show(FragmentManager, String)之外,还有什么正确的方法可以从另一个片段中显示一个DialogFragment呢?


你能提供一下强制关闭的堆栈跟踪吗? - NPike
你能发一下实例化DialogFragment的代码行吗? - IgorGanapolsky
9个回答

55

兼容库中存在一个Bug可能会导致这种情况发生。请尝试在你的DialogFragment中加入以下代码:

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

我还建议将你的对话框片段设置为保留状态,这样在旋转后它就不会被关闭。例如,在onCreate()方法中放置"setRetainInstance(true);"。


11
setRetainInstance(true)可以解决崩溃问题,但是方向改变后对话框会被简单地关闭。同时使用setRetainInstance(true)和上述代码片段会导致对话框重新显示。唯一的问题是一些保存状态的Bundle被搞乱了。方向改变后无法恢复值。每次调用onCreateDialog()时,Bundle永远是null。有什么想法吗? - Weston
1
我没有深入研究过这个问题,因为我只使用了后移库。如果在API 11级别中仍然存在错误,我也不会感到惊讶。我建议在ICS以下的任何情况下都使用后移库,这样您就可以确保片段框架始终正常工作。 - Zsombor Erdődy-Nagy
22
请注意,有报告称 getDialog().setOnDismissListener(null); 在某些设备上会导致崩溃。解决方法是改为调用 getDialog().setDismissMessage(null);。有关详细信息,请参见此问题 - Andy Dennie
3
是的,这会给我造成一个 IllegalStateException: OnDismissListener is already taken by DialogFragment and cannot be replaced. 但是 setDismissMessage 很好用。 - Timmmm
1
@Weston setRetainInstance(true)会将从onSaveInstanceState传递的Bundle设置为null。请参见:https://dev59.com/RmYr5IYBdhLWcg3wvszR#13773496 - Gunnar Karlsson
显示剩余6条评论

10

好的,虽然Zsombor的方法有效,但这是因为我对Fragments不熟悉,他的解决方案会导致saveInstanceState Bundle出现问题。

显然(至少对于DialogFragment而言),它应该是一个public static class。你还必须编写自己的static DialogFragment newInstance()方法。这是因为Fragment类在其instantiate()方法中调用newInstance方法。

因此,总之,你必须按照以下方式编写你的DialogFragments:

public static class MyDialogFragment extends DialogFragment {

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

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

并用以下方式展示它们:

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

这可能是独特于ActionBarSherlock库的,但SDK文档中的官方示例也使用了这种范例。


15
你不需要编写“自己的”newInstance()方法,因为Fragment.instantiate()会调用Class.newInstance()。但是因为你的Fragment实例可能使用Class.newInstance()实例化,所以你必须为你编写的每个Fragment提供显式的默认构造函数。 - Szabolcs Berecz
2
我同意 Szabolcs Berecz 的观点,即DialogFragment不需要静态构造函数,但它需要一个公共的空构造函数(无参数),这样它可以通过Class.newInstance()方法进行正确的初始化。 - Aksel Fatih
非常感谢,这帮助我解决了一个大头疼问题!但我仍然不明白为什么不能像往常一样实例化它。 - lfxgroove
1
@Weston,你为什么说DialogFragment需要是公共静态类?我经常使用DialogFragment,而且没有必要将它们设为静态的。 - IgorGanapolsky
1
@flock.dux:所有类都有一个默认的、无参数构造函数(隐式)。需要默认构造函数的原因是它可以被框架实例化,但初始化 DialogFragment 的正确方式在这个页面上看到的第一段代码中显示了 http://developer.android.com/reference/android/app/DialogFragment.html ,使用创建新实例并为片段设置参数的工厂方法。你将始终是第一个实例化片段的人(使用工厂方法)。然后在 onCreate 中,即使屏幕旋转(例如),你也可以访问参数。 - Jerry Destremps
显示剩余3条评论

2
为了解决Bundle始终为空的问题,我在onSaveInstanceState中将其保存到静态字段中。这是一种不好的代码实现方式,但是我发现这是恢复对话框和保存状态的唯一解决方案。
应该在onDestroy中将Bundle引用设置为null。
@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;
}

这个带有静态成员的类会在该类的类加载器存在时一直保留在内存中,这需要一些时间... - Eugene

1

我使用了提供的解决方案的混合,并添加了一件事情。 这是我的最终解决方案:

我在onCreateDialog中使用了setRetainInstance(true); 我使用了这个:

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

作为savedInstanceState不起作用的解决方法,我创建了一个名为StateHolder的私有类(与listView创建holder的方式相同):

private class StateHolder {
    String name;
    String quantity;
}

我是这样保存状态的:
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
    super.onSaveInstanceState(savedInstanceState);
    stateHolder = new StateHolder();
    stateHolder.name = actvProductName.getText().toString();
    stateHolder.quantity = etProductQuantity.getText().toString();
}

在onDismiss方法中,我将stateHolder设置为null。当对话框创建时,它会验证stateHolder是否为null以恢复状态或正常初始化所有内容。

1
我使用了@ZsomborErdődy-Nagy和@AndyDennie的答案来解决这个问题。你必须继承这个类,在你的父片段中调用setRetainInstance(true)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();
        }
    }

我注意到如果应用程序显示另一个DialogFragment,例如Google Play服务,当方向改变时会导致应用程序崩溃。 - Roger Garzon Nieto

0

我遇到了这个问题,但是 onDestroyView() 技巧并没有起作用。原来是因为我在 onCreate() 中创建了一些相当密集的对话框。这包括保存对 AlertDialog 的引用,然后在 onCreateDialog() 中返回。

当我将所有这些代码移动到 onCreateDialog() 中,并停止保留对对话框的引用时,它又开始工作了。我认为我违反了 DialogFragment 管理其对话框的不变量之一。


0

我曾经遇到过类似的问题,但是上述方法都没有解决我的问题。最终,我需要在代码中创建片段而不是在XML布局中创建。

请参见:替换片段和方向更改


0

我在项目中遇到了这个问题,但以上解决方案都没有起作用。

如果异常看起来像下面这样


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

...

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

这是由于旋转后使用的回退容器 ID 出现问题。请参阅此工单以获取更多详细信息:

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

基本上,您可以通过确保布局中的所有xml片段都有一个标签来防止崩溃发生。这样,在旋转时如果一个片段可见,就可以防止发生回退条件。
在我的情况下,我能够应用这个修复方法而不必重写onDestroyView()或设置setRetainInstance(true),这是针对这种情况的常见建议。

0
在编程中,将以下内容翻译成中文:在onCreate()方法中调用setRetainInstance(true),然后包含这段代码。
@Override
public void onDestroyView() {
    if (getDialog() != null && getRetainInstance()) {
        getDialog().setOnDismissMessage(null);
    }
    super.onDestroyView();
}

当您在onCreate()中调用setRetainInstance(true)时,跨越方向更改时将不再调用onCreate(),但仍将调用onCreateView()。
因此,您仍然可以在onSaveInstanceState()中保存状态到bundle中,然后在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;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接