Prevent dialog dismissal on screen rotation in Android
Asked Answered
R

14

102

I am trying to prevent dialogs built with Alert builder from being dismissed when the Activity is restarted.

If I overload the onConfigurationChanged method I can successfully do this and reset the layout to correct orientation but I lose sticky text feature of edittext. So in solving the dialog problem I have created this edittext problem.

If I save the strings from the edittext and reassign them in the onCofiguration change they still seem to default to initial value not what was entered before rotation. Even if I force an invalidate does seem to update them.

I really need to solve either the dialog problem or the edittext problem.

Thanks for the help.

Remorse answered 26/9, 2011 at 15:14 Comment(2)
How do you save/restore the contents of the edited EditText? Can you show some code?Primipara
I figured out the problem with that, I was forgetting to get the view again by Id after resetting the layout.Remorse
H
143

The best way to avoid this problem nowadays is by using a DialogFragment.

Create a new class which extends DialogFragment. Override onCreateDialog and return your old Dialog or an AlertDialog.

Then you can show it with DialogFragment.show(fragmentManager, tag).

Here's an example with a Listener:

public class MyDialogFragment extends DialogFragment {

    public interface YesNoListener {
        void onYes();

        void onNo();
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        if (!(activity instanceof YesNoListener)) {
            throw new ClassCastException(activity.toString() + " must implement YesNoListener");
        }
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        return new AlertDialog.Builder(getActivity())
                .setTitle(R.string.dialog_my_title)
                .setMessage(R.string.dialog_my_message)
                .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        ((YesNoListener) getActivity()).onYes();
                    }
                })
                .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        ((YesNoListener) getActivity()).onNo();
                    }
                })
                .create();
    }
}

And in the Activity you call:

new MyDialogFragment().show(getSupportFragmentManager(), "tag"); // or getFragmentManager() in API 11+

This answer helps explain these other three questions (and their answers):

Holdover answered 31/3, 2013 at 13:19 Comment(4)
There is a button in my app to call .show(), I have to remember the state of the alert dialog, showing/dismissed. Is there a way to keep the dialog without calling .show()?Printing
It says that onAttach is deprecated now. What should be done instead?Quadrat
@faraz_ahmed_kamran, you should use onAttach(Context context) and android.support.v4.app.DialogFragment. The onAttach method takes context instead of activity as a parameter now.Deeann
There's probably no need for YesNoListener though. See this answer.Bangui
A
49
// Prevent dialog dismiss when orientation changes
private static void doKeepDialog(Dialog dialog){
    WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
    lp.copyFrom(dialog.getWindow().getAttributes());
    lp.width = WindowManager.LayoutParams.WRAP_CONTENT;
    lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
    dialog.getWindow().setAttributes(lp);
}
public static void doLogout(final Context context){     
        final AlertDialog dialog = new AlertDialog.Builder(context)
        .setIcon(android.R.drawable.ic_dialog_alert)
        .setTitle(R.string.titlelogout)
        .setMessage(R.string.logoutconfirm)
        .setPositiveButton("Yes", new DialogInterface.OnClickListener()
        {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                ...   
            }

        })
        .setNegativeButton("No", null)      
        .show();    

        doKeepDialog(dialog);
    }
Actiniform answered 5/12, 2014 at 7:51 Comment(13)
To whom find my code not useful, try it before click :-)Actiniform
Works, don't know how but it works! Clean simple and abstract solution, thanks.Yelp
Amazing solution, very simple unlike the other solutions I found, thank you so much, you helped me prevent my app from crashing and solved a crucial bug!!!Vidette
Cool stuff @ChungIW Now thats coding I adhoreRibose
This used to work well, now on lollipop it doesn't work on some devices, dialog still disappears (sigh)Chutney
@ChungIW, Could you please elaborate? Why do Dialogs usually not survive an orientation change and why does this fix it?Aspic
@StephanHenningsen the Activity gets destroyed on orientation change.Practical
@ChungIW The orientation changes - the Activity gets destroyed - the Activity gets recreated - And your code ensure the Dialog gets recreated. How? Why does it work?Aspic
I think this code is bad. doLogout() has a reference to context which is/contains the activity. The activity cannot be destroyed which can cause a memory leak. I was looking for a possibility to use AlertDialog from static context but now I'm sure that it is impossible. The result can only be garbage I think.Piero
It just looks like it works. The dialog stays open, but it has no connection to the newly created activity or fragment (it gets newly created on each orientation change). So you can't do anything that requires a Context inside the dialog buttons OnClickListener.Zinck
This code works but not recommend at all. It leaks activity reference, which is why the dialog can persistent. This is a very bad practice which will lead to memory leak.Turbot
While this may answer the question it's better to add some description on how this answer may help to solve the issue. Please read How do I write a good answer to know more.Meadowlark
Didn't work for me on Android 11.Galore
V
5

If you're changing the layout on orientation change I wouldn't put android:configChanges="orientation" in your manifest because you're recreating the views anyway.

Save the current state of your activity (like text entered, shown dialog, data displayed etc.) using these methods:

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
}

That way the activity goes through onCreate again and afterwards calls the onRestoreInstanceState method where you can set your EditText value again.

If you want to store more complex Objects you can use

@Override
public Object onRetainNonConfigurationInstance() {
}

Here you can store any object and in onCreate you just have to call getLastNonConfigurationInstance(); to get the Object.

Vaccination answered 26/9, 2011 at 16:45 Comment(2)
OnRetainNonConfigurationInstance() is now deprecated as the Doc says : developer.android.com/reference/android/app/… setRetainInstance(boolean retain) should be use instead : developer.android.com/reference/android/app/…Rosalba
@Rosalba setRetainInstance is completely different: it's for Fragments and it does not guarantee you that instance will be retained.Lungan
H
4

Just add android:configChanges="orientation" with your activity element in AndroidManifest.xml

Example:

<activity
            android:name=".YourActivity"
            android:configChanges="orientation"
            android:label="@string/app_name"></activity>
Hamrah answered 27/6, 2016 at 5:43 Comment(2)
This can cause the dialog to display incorrectly in some circumstances.Grantor
android:configChanges="orientation|screenSize" Note: If your application targets Android 3.2 (API level 13) or higher, then you should also declare the "screenSize" configuration, because it also changes when a device switches between portrait and landscape orientations.Intercalary
A
1

A very easy approach is to create the dialogs from the method onCreateDialog() (see note below). You show them through showDialog(). This way, Android handles the rotation for you and you do not have to call dismiss() in onPause() to avoid a WindowLeak and then you neither have to restore the dialog. From the docs:

Show a dialog managed by this activity. A call to onCreateDialog(int, Bundle) will be made with the same id the first time this is called for a given id. From thereafter, the dialog will be automatically saved and restored.

See Android docs showDialog() for more info. Hope it helps somebody!

Note: If using AlertDialog.Builder, do not call show() from onCreateDialog(), call create() instead. If using ProgressDialog, just create the object, set the parameters you need and return it. In conclusion, show() inside onCreateDialog() causes problems, just create de Dialog instance and return it. This should work! (I have experienced issues using showDialog() from onCreate() -actually not showing the dialog-, but if you use it in onResume() or in a listener callback it works well).

Adelleadelpho answered 5/2, 2012 at 22:29 Comment(5)
For which case would you need some code? The onCreateDialog() or showing it with the builder and calling show() to it?Adelleadelpho
I have managed to do it.. but the thing is, onCreateDialog() is now deprecated :-\Iceblink
OK! Have in mind that most of Android devices still work with 2.X versions, so you can use it anyway! Have a look at Android platform versions usageAdelleadelpho
Still, what's the other option if not onCreateDialog?Iceblink
You can use the builder classes e.g. AlertDialog.Builder. If you use it inside onCreateDialog(), instead of using show() return the result of create(). Else, call show() and store the returned AlertDialog into an attribute of the Activity and in onPause() dismiss() it if showing in order to avoid a WindowLeak. Hope it helps!Adelleadelpho
T
1

This question was answered a long time ago.

Yet this is non-hacky and simple solution I use for myself.

I did this helper class for myself, so you can use it in your application too.

Usage is:

PersistentDialogFragment.newInstance(
        getBaseContext(),
        RC_REQUEST_CODE,
        R.string.message_text,
        R.string.positive_btn_text,
        R.string.negative_btn_text)
        .show(getSupportFragmentManager(), PersistentDialogFragment.TAG);

Or

 PersistentDialogFragment.newInstance(
        getBaseContext(),
        RC_EXPLAIN_LOCATION,
        "Dialog title", 
        "Dialog Message", 
        "Positive Button", 
        "Negative Button", 
        false)
    .show(getSupportFragmentManager(), PersistentDialogFragment.TAG);





public class ExampleActivity extends Activity implements PersistentDialogListener{

        @Override
        void onDialogPositiveClicked(int requestCode) {
                switch(requestCode) {
                  case RC_REQUEST_CODE:
                  break;
                }
        }

        @Override
        void onDialogNegativeClicked(int requestCode) {
                switch(requestCode) {
                  case RC_REQUEST_CODE:
                  break;
                }          
        }
}
Taboret answered 23/6, 2016 at 13:0 Comment(0)
T
1

Definitely, the best approach is by using DialogFragment.

Here is mine solution of wrapper class that helps to prevent different dialogs from being dismissed within one Fragment (or Activity with small refactoring). Also, it helps to avoid massive code refactoring if for some reasons there are a lot of AlertDialogs scattered among the code with slight differences between them in terms of actions, appearance or something else.

public class DialogWrapper extends DialogFragment {
    private static final String ARG_DIALOG_ID = "ARG_DIALOG_ID";

    private int mDialogId;

    /**
     * Display dialog fragment.
     * @param invoker  The fragment which will serve as {@link AlertDialog} alert dialog provider
     * @param dialogId The ID of dialog that should be shown
     */
    public static <T extends Fragment & DialogProvider> void show(T invoker, int dialogId) {
        Bundle args = new Bundle();
        args.putInt(ARG_DIALOG_ID, dialogId);
        DialogWrapper dialogWrapper = new DialogWrapper();
        dialogWrapper.setArguments(args);
        dialogWrapper.setTargetFragment(invoker, 0);
        dialogWrapper.show(invoker.getActivity().getSupportFragmentManager(), null);
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mDialogId = getArguments().getInt(ARG_DIALOG_ID);
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        return getDialogProvider().getDialog(mDialogId);
    }

    private DialogProvider getDialogProvider() {
        return (DialogProvider) getTargetFragment();
    }

    public interface DialogProvider {
        Dialog getDialog(int dialogId);
    }
}

When it comes to Activity you can invoke getContext() inside onCreateDialog(), cast it to the DialogProvider interface and request a specific dialog by mDialogId. All logic to dealing with a target fragment should be deleted.

Usage from fragment:

public class MainFragment extends Fragment implements DialogWrapper.DialogProvider {
    private static final int ID_CONFIRMATION_DIALOG = 0;

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        Button btnHello = (Button) view.findViewById(R.id.btnConfirm);
        btnHello.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                DialogWrapper.show(MainFragment.this, ID_CONFIRMATION_DIALOG);
            }
        });
    }

    @Override
    public Dialog getDialog(int dialogId) {
        switch (dialogId) {
            case ID_CONFIRMATION_DIALOG:
                return createConfirmationDialog(); //Your AlertDialog
            default:
                throw new IllegalArgumentException("Unknown dialog id: " + dialogId);
        }
    }
}

You can read the complete article on my blog How to prevent Dialog being dismissed? and play with the source code.

Tijuana answered 13/1, 2018 at 16:49 Comment(0)
O
1

It seems that this is still an issue, even when "doing everything right" and using DialogFragment etc.

There is a thread on Google Issue Tracker which claims that it is due to an old dismiss message being left in the message queue. The provided workaround is quite simple:

    @Override
    public void onDestroyView() {
        /* Bugfix: https://issuetracker.google.com/issues/36929400 */
        if (getDialog() != null && getRetainInstance())
            getDialog().setDismissMessage(null);

        super.onDestroyView();
    }

Incredible that this is still needed 7 years after that issue was first reported.

Obstructionist answered 17/9, 2018 at 19:35 Comment(1)
See also #14657990.Leesaleese
D
0

You can combine the Dialog's onSave/onRestore methods with the Activity's onSave/onRestore methods to keep the state of the Dialog.

Note: This method works for those "simple" Dialogs, such as displaying an alert message. It won't reproduce the contents of a WebView embedded in a Dialog. If you really want to prevent a complex dialog from dismissal during rotation, try Chung IW's method.

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
     super.onRestoreInstanceState(savedInstanceState);
     myDialog.onRestoreInstanceState(savedInstanceState.getBundle("DIALOG"));
     // Put your codes to retrieve the EditText contents and 
     // assign them to the EditText here.
}

@Override
protected void onSaveInstanceState(Bundle outState) {
     super.onSaveInstanceState(outState);
     // Put your codes to save the EditText contents and put them 
     // to the outState Bundle here.
     outState.putBundle("DIALOG", myDialog.onSaveInstanceState());
}
Disastrous answered 3/2, 2015 at 23:57 Comment(0)
G
0

I had a similar problem: when the screen orientation changed, the dialog's onDismiss listener was called even though the user didn't dismiss the dialog. I was able to work around this by instead using the onCancel listener, which triggered both when the user pressed the back button and when the user touched outside of the dialog.

Grantor answered 30/1, 2018 at 8:28 Comment(0)
U
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.

Untold answered 8/12, 2020 at 11:12 Comment(0)
L
0

Ok After working on this for 1 Week I found the solution:

Of course the correct solution to above issue is DialogFragement Still there was some issues I was facing as I wanted to make it resusable and sometimes it was not dismissing after any system change like rotating, changing of dark to light mode (Was able to continue appearing in screen but dismissing was disabled) etc..

This problem will resolve issues :

  1. Disappearing of Dialog when screen rotates/System changes.
  2. Data will persistent on dialog (Dialog Title and other stuff).
  3. Will be able to dismiss after system changes/rotate.

Note: I have done this in Kotlin. But concept is same please read comments its very important


ConfirmDialogWithYesAndNo.kt

class ConfirmDialogWithYesAndNo(private val confirmDialogTitleText:String="Default Confirm Dialog Title",private val confirmDialogMessageText:String="Default Confirm Dialog Message",private val confirmDialogPositiveButtonText:String="Yes",private val confirmDialogNegativeButtonText:String="No",private val confirmDialogPositiveButtonClickListener:()->Unit= {},private val confirmDialogNegativeButtonClickListener:()->Unit= {}): DialogFragment(){
    //Must keep this global
    private lateinit var alertDialog: AlertDialog

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //Must use this otherwise after system change it will not dismiss and won't have same text which you passed.
        retainInstance = true
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        super.onCreateDialog(savedInstanceState)

        //Normal Dialog creation and return it
        val builder = AlertDialog.Builder(requireActivity())
        builder.setTitle(confirmDialogTitleText)
        builder.setMessage(confirmDialogMessageText)
        builder.setIcon(android.R.drawable.ic_dialog_alert)
        builder.setPositiveButton("Yes") { _, _ ->
            //In case you use custom layout
            alertDialog.dismiss()

            confirmDialogPositiveButtonClickListener()
        }
        builder.setNegativeButton("No") { _, _ ->
            //In case you use custom layout button
            confirmDialogNegativeButtonClickListener()
        }
        val alertDialog: AlertDialog = builder.create()
        alertDialog.setCancelable(false)
        alertDialog.show()

        return alertDialog
    }
}

Usage

MainActivity.kt:

class MainActivity : AppCompatActivity() {
    ....
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
.....
        butoon.setOnClickListener {
            val confirmDialogWithYesAndNo= ConfirmDialogWithYesAndNo("Hiii T","Hi Message","Yess B","No B",{
                Toast.makeText(this,"Yesss",Toast.LENGTH_LONG).show()
            },{
                Toast.makeText(this,"Noo",Toast.LENGTH_LONG).show()
            })
            confirmDialogWithYesAndNo.showNow(this.supportFragmentManager,"anyrandomstring")
          ...
        }
       ....

    }
}


To contact or to check this code example : vaibhavmojidra.github.io/site/

Larva answered 6/1 at 13:18 Comment(0)
R
-1

Yes, I agree with the solution of using DialogFragment given by @Brais Gabin, just want to suggest some changes to the solution given by him.

While defining our custom class that extends DialogFragment, we require some interfaces to manage the actions ultimately by the activity or the fragment that has invoked the dialog. But setting these listener interfaces in the onAttach(Context context) method may sometimes cause ClassCastException that may crash the app.

So to avoid this exception, we can create a method to set the listener interfaces and call just it after creating the object of the dialog fragment. Here is a sample code that could help you understand more-

AlertRetryDialog.class

    public class AlertRetryDialog extends DialogFragment {

       public interface Listener{
         void onRetry();
         }

    Listener listener;

     public void setListener(Listener listener)
       {
       this.listener=listener;
       }

    @NonNull
    @Override
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
    AlertDialog.Builder builder=new AlertDialog.Builder(getActivity());
    builder.setMessage("Please Check Your Network Connection").setPositiveButton("Retry", new 
    DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
             //Screen rotation will cause the listener to be null
            //Always do a null check of your interface listener before calling its method
            if(listener!=null&&listener instanceof HomeFragment)
            listener.onRetry();
        }
       }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            dialog.dismiss();
         }
     });
     return builder.create();
    }

   }

And in the Activity or in the Fragment you call-

                   AlertRetryDialog alertRetryDialog = new AlertRetryDialog();
                    alertRetryDialog.setListener(HomeFragment.this);
                    alertRetryDialog.show(getFragmentManager(), "tag");

And implement the methods of your listener interface in your Activity or the Fragment-

              public class YourActivity or YourFragment implements AlertRetryDialog.Listener{ 
                
                  //here's my listener interface's method
                    @Override
                    public void onRetry()
                    {
                     //your code for action
                      }
                
                 }

Always make sure that you do a null check of the listener interfaces before calling any of its methods to prevent NullPointerException (Screen rotation will cause the listener interfaces to be null).

Please do let me know if you find this answer helpful. Thank You.

Retentivity answered 15/5, 2021 at 10:34 Comment(0)
G
-3

Just use

ConfigurationChanges = Android.Content.PM.ConfigChanges.Orientation | Android.Content.PM.ConfigChanges.ScreenSize

and app will know how to handle rotation and screen size.

Giltedged answered 2/12, 2016 at 17:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.