How to properly retain a DialogFragment through rotation?
Asked Answered
T

5

77

I have a FragmentActivity that hosts a DialogFragment.

The DialogFragment perform network requests and handles Facebook authentication, so I need to retain it during rotation.

I've read all the other questions relating to this issue, but none of them have actually solved the problem.

I'm using putFragment and getFragment to save the Fragment instance and get it again during activity re-creation.

However, I'm always getting a null pointer exception on the call to getFragment in onRestoreInstanceState. I would also like to keep the dialog from being dismissed during rotation, but so far I can't even retain the instance of it.

Any ideas what's going wrong?

Here's what my code currently looks like:

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);
    }

}

This is the exception stack trace:

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)
Tourniquet answered 2/2, 2013 at 0:58 Comment(3)
Can you give us the exception stack trace? I think you might want to focus on this aspect of the problem.Ally
What happens when you delete the calls to putFragment and getFragment? If the DialogFragment is currently showing on the screen, the Fragment's state should be restored upon configuration change.Portrait
I think the NullPointerException will go away if you add a call to super.onSaveInstanceState(outState) in your overridden onSaveInstanceState method.Dimarco
C
149

Inside your DialogFragment, call Fragment.setRetainInstance(boolean) with the value true. You don't need to save the fragment manually, the framework already takes care of all of this. Calling this will prevent your fragment from being destroyed on rotation and your network requests will be unaffected.

You may have to add this code to stop your dialog from being dismissed on rotation, due to a bug with the compatibility library:

@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();
}
Chrysler answered 16/3, 2013 at 1:15 Comment(7)
This did the trick. I think what I was doing wrong previously was that I needed to wrap the showDialg() code inside onCreateView with a null check on savedInstanceStateTourniquet
getting error- java.lang.RuntimeException: Unable to destroy activity {com.attchment/com.attchment.MainActivity}: java.lang.IllegalStateException: OnDismissListener is already taken by DialogFragment and can not be replaced.Morissa
Hey Google, c'mon, this ain't rocket science. Why don't ya fix it? :)Aikido
@Aikido Google is not famous for fixing bugs quickly. CoordinatorView is full of bugs that have not been fixed in 2 years.. Had a bug in MapFragment that got fixed after 3 years. At least the MapFragment bug got fixed eventually :)Outgrowth
Fragment.java says this about setRetainInstance: "This can only be used with fragments not in the back stack." DialogFragment is definitely in the back stack, see show. Does anyone know if this code comment is plain wrong?Garlandgarlanda
This is no longer a viable solution since setRetainInstance is deprecated...Hack
Does it prevent the override fun onDismiss to be called?Sapienza
A
15

One of the advantages of using dialogFragment compared to just using alertDialogBuilder is exactly because dialogfragment can automatically recreate itself upon rotation without user intervention.

However, when the dialogfragment does not recreate itself, it is possible that you overwrite onSaveInstanceState but didn't to call super:

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState); // <-- must call this if you want to retain dialogFragment upon rotation
    ...
}
Abyss answered 23/9, 2013 at 8:13 Comment(1)
+1 on that, just to mention that in my experience, this applies to views and we still need to save variablesWiesbaden
S
11

This is a convenience method using the fix from antonyt's answer:

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();
    }
}

Just let your DialogFragment extend this class and everything will be fine. This becomes especially handy, if you have multiple DialogFragments in your project which all need this fix.

Sitsang answered 28/5, 2017 at 16:47 Comment(0)
H
1

Most of the answers here are incorrect because they use setRetainInstance(true), but this is now deprecated as of API 28. Here is the solution I am using:

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

If the function returns false, then simply call dialog.show(fm, "<FRAGMENT_TAG>") to show it again.

Hack answered 18/8, 2021 at 18:22 Comment(0)
A
0

In case nothing helps, and you need a solution that works, you can go on the safe side, and each time you open a dialog save its basic info to the activity ViewModel (and remove it from this list when you dismiss dialog). This basic info could be dialog type and some id (the information you need in order to open this dialog). This ViewModel is not destroyed during changes of Activity lifecycle. Let's say user opens a dialog to leave a reference to a restaurant. So dialog type would be LeaveReferenceDialog and the id would be the restaurant id. When opening this dialog, you save this information in an Object that you can call DialogInfo, and add this object to the ViewModel of the Activity. This information will allow you to reopen the dialog when the activity onResume() is being called:

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

Which calls:

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

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

    mainActivityViewModel.setIsRestoringDialogs(false) // open lock
}

When IsRestoringDialogs in ViewModel is set to true, dialog info will not be added to the list in view model, and it's important because we're now restoring dialogs which are already in that list. Otherwise, changing the list while using it would cause an exception. So:

// 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))
    }

Then remove dialog info when dismissing it:

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

And in the ViewModel of the 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)
}

You actually reopen all the dialogs that were open before, in the same order. But how do they retain their information? Each dialog has a ViewModel of its own, which is also not destroyed during the activity lifecycle. So when you open the dialog, you get the ViewModel and init the UI using this ViewModel of the dialog as always.

Abubekr answered 8/12, 2020 at 10:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.