Как правильно сохранить 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)

person ch3rryc0ke    schedule 02.02.2013    source источник
comment
Можете ли вы дать нам трассировку стека исключений? Я думаю, вы могли бы захотеть сосредоточиться на этом аспекте проблемы.   -  person Brian Attwell    schedule 02.02.2013
comment
Что происходит, когда вы удаляете вызовы putFragment и getFragment? Если DialogFragment в данный момент отображается на экране, состояние фрагмента должно быть восстановлено после изменения конфигурации.   -  person user697495    schedule 06.02.2013
comment
Я думаю, что исключение NullPointerException исчезнет, ​​если вы добавите вызов super.onSaveInstanceState(outState) в свой переопределенный метод onSaveInstanceState.   -  person burnttoast11    schedule 07.02.2013


Ответы (4)


Внутри вашего 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();
}
person antonyt    schedule 16.03.2013
comment
Это помогло. Я думаю, что раньше я делал неправильно, так это то, что мне нужно было обернуть код showDialg() внутри onCreateView с нулевой проверкой на saveInstanceState - person ch3rryc0ke; 21.03.2013
comment
получение ошибки - java.lang.RuntimeException: невозможно уничтожить активность {com.attchment/com.attchment.MainActivity}: java.lang.IllegalStateException: OnDismissListener уже занят DialogFragment и не может быть заменен. - person abh22ishek; 23.01.2015
comment
Эй, Google, да ладно, это не ракетостроение. Почему бы тебе не исправить это? :) - person Diego; 20.02.2015
comment
@Diego Google не славится быстрым исправлением ошибок. CoordinatorView полон ошибок, которые не были исправлены за 2 года. Была ошибка в MapFragment, которая была исправлена ​​через 3 года. По крайней мере, ошибка MapFragment в конце концов была исправлена ​​:) - person A. Steenbergen; 17.04.2017
comment
Fragment.java говорит о setRetainInstance следующее: Это можно использовать только с фрагментами, не находящимися в заднем стеке. DialogFragment определенно находится в заднем стеке, см. show. Кто-нибудь знает, является ли этот комментарий к коду ошибочным? - person androidguy; 18.04.2018

Одно из преимуществ использования 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
    ...
}
person Neoh    schedule 23.09.2013
comment
+1 за это, просто чтобы упомянуть, что, по моему опыту, это относится к представлениям, и нам все еще нужно сохранять переменные. - person fullmoon; 15.01.2017

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

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

person Willi Mentzel    schedule 28.05.2017

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

Если для параметра 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 действия:

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 диалогового окна, как всегда.

person Amir Golan    schedule 08.12.2020