Як правильно утримувати DialogFragment за допомогою обертання?


75

У мене є 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)

Чи можете ви дати нам трасування стека винятків? Я думаю, ви можете зосередитись на цьому аспекті проблеми.
Брайан Еттвелл

Що відбувається, коли ви видаляєте виклики putFragment і getFragment? Якщо діалоговий фрагмент зараз відображається на екрані, стан фрагмента слід відновити після зміни конфігурації.
user697495

Я думаю, що NullPointerException зникне, якщо ви додасте виклик до super.onSaveInstanceState(outState)свого перевизначеного onSaveInstanceStateметоду.
burnttoast11

Відповіді:


147

Усередині 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();
}

Це зробило фокус. Я думаю, що раніше я робив неправильно, що мені потрібно було обернути код showDialg () всередині onCreateView з нульовою перевіркою saveInstanceState
ch3rryc0ke

отримання помилки- java.lang.RuntimeException: Неможливо знищити активність {com.attchment / com.attchment.MainActivity}: java.lang.IllegalStateException: OnDismissListener вже зайнятий DialogFragment і не може бути замінений.
abh22ishek

13
Гей, Google, давай, це не ракетна наука. Чому ти не виправляєш? :)
Дієго,

1
@Diego Google не славиться тим, що швидко виправляє помилки. CoordinatorView переповнений помилками, які не були виправлені за 2 роки .. Була помилка в MapFragment, яка виправлена ​​через 3 роки. Принаймні, помилка MapFragment врешті виправлена ​​:)
A. Steenbergen

Fragment.javaговорить про це setRetainInstance: "Це можна використовувати лише з фрагментами, що не знаходяться в задньому стеку." DialogFragment, безумовно, знаходиться в задньому стеці, див show. Хтось знає, чи цей коментар до коду неправдивий?
androidguy

16

Однією з переваг використання dialogFragmentпорівняно з просто використанням alertDialogBuilderє саме те, що діафрагмент може автоматично відтворити себе при обертанні без втручання користувача.

Однак, коли діалоговий фрагмент не відтворюється, можливо, ви перезаписали, onSaveInstanceStateале не викликали super:

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

1
+1 на цьому, лише щоб згадати, що на моєму досвіді це стосується переглядів, і нам все одно потрібно зберігати змінні
Бадр,

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у вашому проекті є декілька, які всі потребують цього виправлення.


0

Якщо нічого не допомагає, і вам потрібне рішення, яке працює, ви можете перейти до безпечної сторони, і кожного разу, коли ви відкриваєте діалогове вікно, зберігайте його основну інформацію в дії 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 of 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 та запускаєте інтерфейс, використовуючи цей ViewModel діалогового вікна, як завжди.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.