如何通过旋转正确保留 DialogFragment?

Posted

技术标签:

【中文标题】如何通过旋转正确保留 DialogFragment?【英文标题】:How to properly retain a DialogFragment through rotation? 【发布时间】:2013-01-17 10:13:27 【问题描述】:

我有一个托管 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 ComponentInfoio.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)

【问题讨论】:

你能给我们异常堆栈跟踪吗?我认为您可能需要关注问题的这一方面。 删除对 putFragment 和 getFragment 的调用时会发生什么?如果 DialogFragment 当前正在屏幕上显示,则应该在配置更改时恢复 Fragment 的状态。 我认为如果您在覆盖的 onSaveInstanceState 方法中添加对 super.onSaveInstanceState(outState) 的调用,NullPointerException 将会消失。 【参考方案1】:

在您的DialogFragment 中,使用值true 调用Fragment.setRetainInstance(boolean)。您不需要手动保存片段,框架已经处理了所有这些。调用这将防止您的片段在旋转时被破坏,并且您的网络请求将不受影响。

由于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();

【讨论】:

这成功了。我认为我之前做错的事情是我需要将 showDialg() 代码包装在 onCreateView 中,并对 savedInstanceState 进行空检查 出现错误-java.lang.RuntimeException:无法销毁活动 com.attchment/com.attchment.MainActivity:java.lang.IllegalStateException:OnDismissListener 已被 DialogFragment 占用,无法替换. 嘿谷歌,来吧,这不是火箭科学。你为什么不修呢? :) @Diego Google 并不以快速修复错误而闻名。 CoordinatorView 充满了 2 年未修复的错误。在 MapFragment 中有一个错误在 3 年后得到修复。至少 MapFragment 错误最终得到了修复 :) 这不再是一个可行的解决方案,因为 setRetainInstance 已被弃用...【参考方案2】:

与仅使用alertDialogBuilder 相比,使用dialogFragment 的优势之一正是因为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,仅提一下,根据我的经验,这适用于视图,我们仍然需要保存变量【参考方案3】:

这是使用安东尼回答中的修复的便捷方法:

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 都需要此修复,这将变得特别方便。

【讨论】:

【参考方案4】:

如果没有任何帮助,并且您需要一个可行的解决方案,您可以采取安全措施,每次打开对话框时都将其基本信息保存到活动 ViewModel(并在您关闭对话框时将其从该列表中删除)。此基本信息可能是对话框类型和一些 id(打开此对话框所需的信息)。此 ViewModel 在 Activity 生命周期更改期间不会被销毁。假设用户打开一个对话框以留下对餐厅的引用。所以对话类型是 LeaveReferenceDialog,id 是餐厅 id。打开此对话框时,将此信息保存在可以调用 DialogInfo 的 Object 中,并将此对象添加到 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,它在 Activity 生命周期中也不会被销毁。所以当你打开对话框时,你会得到 ViewModel 并像往常一样使用对话框的这个 ViewModel 来初始化 UI。

【讨论】:

【参考方案5】:

这里的大多数答案都是不正确的,因为它们使用 setRetainInstance(true),但现在已弃用 API 28。这是我正在使用的解决方案:

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

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

【讨论】:

以上是关于如何通过旋转正确保留 DialogFragment?的主要内容,如果未能解决你的问题,请参考以下文章

如何创建没有标题的 DialogFragment?

导航到 Jetpack Navigation 中的另一个 Fragment 后将 DialogFragment 保留在 backstack 中

Android--从零单排系列--常用对话框和DialogFragment的优势

如何使用RxJava管理DialogFragment?

使用 viewmodel 旋转设备时如何保留我的 edittext 数据?

旋转时调用 DatePickerDialog onDateSet