如何在旋转屏幕时正确保留DialogFragment?

77

我有一个托管DialogFragment的FragmentActivity。

DialogFragment执行网络请求并处理Facebook身份验证,因此我需要在旋转期间保留它。

我已经阅读了所有其他与此问题相关的问题,但它们都没有解决问题。

我正在使用putFragment和getFragment来保存Fragment实例并在活动重新创建时获取它。

然而,在onRestoreInstanceState中调用getFragment时,我总是得到空指针异常。我还想在旋转期间保持对话框不被关闭,但到目前为止我甚至无法保留它的实例。

有什么想法出了问题吗?

这是我的当前代码:

public class OKLoginActivity extends FragmentActivity implements OKLoginDialogListener
{

    private OKLoginFragment loginDialog;
    private static final String TAG_LOGINFRAGMENT = "OKLoginFragment";


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

        FragmentManager fm = getSupportFragmentManager();

        if(savedInstanceState == null)
        {
            loginDialog = new OKLoginFragment(); 
            loginDialog.show(fm, TAG_LOGINFRAGMENT);
        }
    }


    @Override
    public void onSaveInstanceState(Bundle outState)
    {
        getSupportFragmentManager().putFragment(outState,TAG_LOGINFRAGMENT, loginDialog);
    }

    @Override
    public void onRestoreInstanceState(Bundle inState)
    {
        FragmentManager fm = getSupportFragmentManager();
        loginDialog = (OKLoginFragment) fm.getFragment(inState, TAG_LOGINFRAGMENT);
    }

}

这是异常堆栈跟踪:

02-01 16:31:13.684: E/AndroidRuntime(9739): FATAL EXCEPTION: main
02-01 16:31:13.684: E/AndroidRuntime(9739): java.lang.RuntimeException: Unable to start activity ComponentInfo{io.openkit.example.sampleokapp/io.openkit.OKLoginActivity}: java.lang.NullPointerException
02-01 16:31:13.684: E/AndroidRuntime(9739):     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2180)
02-01 16:31:13.684: E/AndroidRuntime(9739):     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2230)
02-01 16:31:13.684: E/AndroidRuntime(9739):     at android.app.ActivityThread.handleRelaunchActivity(ActivityThread.java:3692)
02-01 16:31:13.684: E/AndroidRuntime(9739):     at android.app.ActivityThread.access$700(ActivityThread.java:141)
02-01 16:31:13.684: E/AndroidRuntime(9739):     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1240)
02-01 16:31:13.684: E/AndroidRuntime(9739):     at android.os.Handler.dispatchMessage(Handler.java:99)
02-01 16:31:13.684: E/AndroidRuntime(9739):     at android.os.Looper.loop(Looper.java:137)
02-01 16:31:13.684: E/AndroidRuntime(9739):     at android.app.ActivityThread.main(ActivityThread.java:5039)
02-01 16:31:13.684: E/AndroidRuntime(9739):     at java.lang.reflect.Method.invokeNative(Native Method)
02-01 16:31:13.684: E/AndroidRuntime(9739):     at java.lang.reflect.Method.invoke(Method.java:511)
02-01 16:31:13.684: E/AndroidRuntime(9739):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
02-01 16:31:13.684: E/AndroidRuntime(9739):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:560)
02-01 16:31:13.684: E/AndroidRuntime(9739):     at dalvik.system.NativeStart.main(Native Method)
02-01 16:31:13.684: E/AndroidRuntime(9739): Caused by: java.lang.NullPointerException
02-01 16:31:13.684: E/AndroidRuntime(9739):     at android.support.v4.app.FragmentManagerImpl.getFragment(FragmentManager.java:528)
02-01 16:31:13.684: E/AndroidRuntime(9739):     at io.openkit.OKLoginActivity.onRestoreInstanceState(OKLoginActivity.java:62)
02-01 16:31:13.684: E/AndroidRuntime(9739):     at android.app.Activity.performRestoreInstanceState(Activity.java:910)
02-01 16:31:13.684: E/AndroidRuntime(9739):     at android.app.Instrumentation.callActivityOnRestoreInstanceState(Instrumentation.java:1131)
02-01 16:31:13.684: E/AndroidRuntime(9739):     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2158)

你能给我们提供异常堆栈跟踪吗?我认为你可能想要关注问题的这个方面。 - Brian Attwell
当您删除对putFragment和getFragment的调用时会发生什么?如果DialogFragment当前正在屏幕上显示,则在配置更改时应恢复Fragment的状态。 - user697495
如果您在重写的onSaveInstanceState方法中添加对super.onSaveInstanceState(outState)的调用,我认为NullPointerException问题将会消失。 - burnttoast11
5个回答

149

在你的DialogFragment中,调用Fragment.setRetainInstance(boolean)并将值设置为true。不需要手动保存片段,框架已经处理了所有这些事情。调用此方法将防止你的片段在旋转时被销毁,同时网络请求不受影响。

由于与兼容性库相关的bug,你可能需要添加以下代码来防止在旋转时关闭对话框:

@Override
public void onDestroyView() {
    Dialog dialog = getDialog();
    // handles https://code.google.com/p/android/issues/detail?id=17423
    if (dialog != null && getRetainInstance()) {
        dialog.setDismissMessage(null);
    }
    super.onDestroyView();
}

这个方法很管用。我认为我之前做错的是需要在onCreateView中用savedInstanceState进行空值检查,然后将showDialog()代码包装起来。 - ch3rryc0ke
1
出现错误 - java.lang.RuntimeException: 无法销毁活动 {com.attchment / com.attchment.MainActivity}: java.lang.IllegalStateException:OnDismissListener 已经由 DialogFragment 占用,无法替换。 - abh22ishek
13
嘿,谷歌,这不是火箭科学。你为什么不修复它呢? :) - Diego
1
@Diego 谷歌并不以快速修复漏洞而闻名。CoordinatorView充满了2年未被修复的漏洞。在MapFragment中有一个漏洞,经过3年才得到修复。至少MapFragment的漏洞最终得到了修复 :) - A. Steenbergen
2
由于setRetainInstance已被弃用,这不再是可行的解决方案... - DummyData
显示剩余2条评论

15

使用 DialogFragment 相比于仅使用 AlertDialogBuilder 的优点之一是,DialogFragment 可以在旋转时自动重新创建自己,无需用户干预。

但是,当 DialogFragment 没有重新创建时,您可能会覆盖 onSaveInstanceState,但未调用 super

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState); // <-- must call this if you want to retain dialogFragment upon rotation
    ...
}

1
+1,只是想提一下根据我的经验,这适用于视图,我们仍然需要保存变量。 - fullmoon

11

这是一个使用antonyt答案中修复的便利方法:

public class RetainableDialogFragment extends DialogFragment {

    public RetainableDialogFragment() {
        setRetainInstance(true);
    }

    @Override
    public void onDestroyView() {
        Dialog dialog = getDialog();
        // handles https://code.google.com/p/android/issues/detail?id=17423
        if (dialog != null && getRetainInstance()) {
            dialog.setDismissMessage(null);
        }
        super.onDestroyView();
    }
}

只需让您的 DialogFragment 扩展此类,一切都会变得不错。如果您的项目中有多个需要此修复程序的DialogFragments,则这将变得尤为方便。


1

这里大部分的答案都是错误的,因为它们使用了setRetainInstance(true),但是自API 28以来,这已经被弃用了。这是我正在使用的解决方案:

fun isDialogVisible(fm: FragmentManager): Boolean {
    val dialog = fm.findFragmentByTag("<FRAGMENT_TAG>")
    return dialog?.isResumed ?: false
}

如果函数返回false,则只需调用 dialog.show(fm, "<FRAGMENT_TAG>") 再次显示它。

0

如果没有任何帮助,而你需要一个可行的解决方案,你可以选择保险起见,每次打开对话框时将其基本信息保存到活动 ViewModel 中(并在关闭对话框时从此列表中删除)。这些基本信息可以是对话框类型和一些 ID(你需要打开此对话框的信息)。这个 ViewModel 在 Activity 生命周期的更改期间不会被销毁。假设用户打开一个对话框来留下对餐厅的评价。那么对话框类型将是 LeaveReferenceDialog,ID 将是餐厅 ID。当打开此对话框时,你将此信息保存在一个对象中,你可以称之为 DialogInfo,并将此对象添加到 Activity 的 ViewModel 中。这些信息将允许你在 onResume() 被调用时重新打开对话框:

// On resume in Activity
    override fun onResume() {
            super.onResume()
    
            // Restore dialogs that were open before activity went to background
            restoreDialogs()
        }

调用哪些函数:

    fun restoreDialogs() {
    mainActivityViewModel.setIsRestoringDialogs(true) // lock list in view model

    for (dialogInfo in mainActivityViewModel.openDialogs)
        openDialog(dialogInfo)

    mainActivityViewModel.setIsRestoringDialogs(false) // open lock
}

当 ViewModel 中的 IsRestoringDialogs 设置为 true 时,对话框信息将不会添加到视图模型中的列表中,这很重要,因为我们现在正在恢复已经在该列表中的对话框。否则,在使用列表时更改列表会导致异常。所以:

// Create new dialog
        override fun openLeaveReferenceDialog(restaurantId: String) {
            var dialog = LeaveReferenceDialog()
            // Add id to dialog in bundle
            val bundle = Bundle()
            bundle.putString(Constants.RESTAURANT_ID, restaurantId)
            dialog.arguments = bundle
            dialog.show(supportFragmentManager, "")
        
            // Add dialog info to list of open dialogs
            addOpenDialogInfo(DialogInfo(LEAVE_REFERENCE_DIALOG, restaurantId))
    }

然后在关闭对话框时删除对话框信息:

// Dismiss dialog
override fun dismissLeaveReferenceDialog(Dialog dialog, id: String) {
   if (dialog?.isAdded()){
      dialog.dismiss()
      mainActivityViewModel.removeOpenDialog(LEAVE_REFERENCE_DIALOG, id)
   }
}

在 Activity 的 ViewModel 中:

fun addOpenDialogInfo(dialogInfo: DialogInfo){
    if (!isRestoringDialogs){
       val dialogWasInList = removeOpenDialog(dialogInfo.type, dialogInfo.id)
       openDialogs.add(dialogInfo)
     }
}


fun removeOpenDialog(type: Int, id: String) {
    if (!isRestoringDialogs)
       for (dialogInfo in openDialogs) 
         if (dialogInfo.type == type && dialogInfo.id == id) 
            openDialogs.remove(dialogInfo)
}

你实际上重新打开了之前打开的所有对话框,按照相同的顺序。但是它们如何保留它们的信息呢?每个对话框都有自己的ViewModel,在活动生命周期期间也不会被销毁。因此,当您打开对话框时,您获取ViewModel并像往常一样使用该对话框的ViewModel来初始化UI。

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