Как правильно сохранить DialogFragment через ротацию?

У меня есть FragmentActivity, в котором размещается DialogFragment.

DialogFragment выполняет сетевые запросы и обрабатывает аутентификацию Facebook, поэтому мне нужно сохранить ее во время ротации.

Я прочитал все другие вопросы, касающиеся этой проблемы, но ни один из них фактически не решил проблему.

Я использую putFragment и getFragment, чтобы сохранить экземпляр Fragment и получить его снова во время повторного создания действия.

Тем не менее, я всегда получаю исключение нулевого указателя при вызове getFragment в onRestoreInstanceState. Я также хотел бы не допустить закрытия диалогового окна во время ротации, но пока не могу даже сохранить его экземпляр.

Есть идеи, что не так?

Вот как выглядит мой код:

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)

5 ответов

Решение

Внутри вашего DialogFragment, вызов Fragment.setRetainInstance(boolean) со значением 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 по сравнению с просто использованием alertDialogBuilder Именно потому, что dialogfragment может автоматически воссоздать себя при вращении без вмешательства пользователя.

Однако когда фрагмент диалога не воссоздает сам себя, возможно, вы перезаписали onSaveInstanceState но не позвонил super:

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

Это удобный метод, использующий исправление из ответа Антонита:

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 в вашем проекте, которые все нуждаются в этом исправлении.

Большинство ответов здесь неверны, потому что они используют 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>"), чтобы показать его снова.

Если ничего не помогает и вам нужно работающее решение, вы можете перестраховаться и каждый раз, когда открываете диалоговое окно, сохраняйте его основную информацию в ViewModel действия (и удаляйте ее из этого списка при закрытии диалогового окна). Этой базовой информацией может быть тип диалога и некоторый идентификатор (информация, необходимая для открытия этого диалога). Эта ViewModel не разрушается при изменении жизненного цикла Activity. Допустим, пользователь открывает диалоговое окно, чтобы оставить ссылку на ресторан. Таким образом, тип диалога будет LeaveReferenceDialog, а id будет идентификатором ресторана. При открытии этого диалогового окна вы сохраняете эту информацию в объекте, который вы можете вызвать DialogInfo, и добавляете этот объект в ViewModel Activity. Эта информация позволит вам повторно открыть диалоговое окно при вызове действия 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
}

Когда IsRestoringDialogs в ViewModel имеет значение 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)
   }
}

И в ViewModel Activity:

      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 и запускаете UI, как всегда, используя эту ViewModel диалога.

Другие вопросы по тегам