Dialogs in Android: Practical Guide

By | 2019-05-31T19:11:47+00:00 May 31st, 2019|Android development|0 Comments

Dialogs are among the most basic building blocks in Android applications. They allow you to show important notifications to your users, quickly capture users’ input, expand screens’ functionality and much more. Unfortunately, as basic as they are, in Android dialogs aren’t that simple to implement.

In this post I’ll try to summarize what you need to know in order to use dialogs safely and efficiently in your Android apps.

Dialog vs DialogFragment

The very first question to address in respect to Android dialogs is: “should I use Dialog or DialogFragment to implement dialogs?”.

Initially there was just Dialog class in Android. If you’d like to have dialogs, you’d use it and that’s it. [Well, with the exception of developers who implemented dialogs infrastructure manually by using View subclasses, but I wouldn’t recommend this approach.] Then, when Fragments came around, DialogFragment was introduced. This is basically a Fragment that can be used as a dialog.

I recommend using DialogFragment over Dialog due to the following reasons:

  • DialogFragment is automatically re-created after configuration changes and save & restore flow
  • DialogFragment inherits full Fragment’s lifecycle

Automatic re-creation is very handy because you probably want most of your dialogs to remain on the screen if, for example, the device is rotated. With Dialog you’d need to take care of this manually because Dialog instances are dismissed when their host Activity is destroyed. DialogFragment instances are destroyed too, but then Android re-creates them for you after the host Activity is re-created.

The point about full Fragment’s lifecycle might be a bit surprising because Fragment’s lifecycle is a very complicated topic. It’s rarely brought up as something positive. However, by having a full lifecycle, DialogFragment instances are much more suited to be fully-capable controllers. This might be not that important for simple dialogs with just one or two buttons, but becomes very handy when you need to build complex dialogs that, for example, allow the user to invoke some long-running operation, show progress indication and then display the results.

So, in my opinion it’s best to implement your dialogs using DialogFragment instances.

How to Implement DialogFragment

There are many good tutorials about DialogFragment on the internet, so I won’t go into much details here. However, I’d like to point out several nuances and discuss some high-level design considerations.

There are two ways to define DialogFragment’s UI:

  • return View hierarchy from onCreateView() method (the standard Fragment approach)
  • constuct a Dialog and return it from onCreateDialog()

The second approach allows you to reuse existing subclasses of Dialog in Android (e.g. AlertDialog), as well as third-party Dialog libraries.

To pass runtime parameters into my dialogs (e.g. strings), I usually use static factory method:

public class PromptDialog extends BaseDialog {

    protected static final String ARG_TITLE = "ARG_TITLE";
    protected static final String ARG_MESSAGE = "ARG_MESSAGE";
    protected static final String ARG_POSITIVE_BUTTON_CAPTION = "ARG_POSITIVE_BUTTON_CAPTION";
    protected static final String ARG_NEGATIVE_BUTTON_CAPTION = "ARG_NEGATIVE_BUTTON_CAPTION";

    public static PromptDialog newInstance(String title, 
                                           String message, 
                                           String positiveButtonCaption, 
                                           String negativeButtonCaption) {
        PromptDialog promptDialog = new PromptDialog();
        Bundle args = new Bundle(4);
        args.putString(ARG_TITLE, title);
        args.putString(ARG_MESSAGE, message);
        args.putString(ARG_POSITIVE_BUTTON_CAPTION, positiveButtonCaption);
        args.putString(ARG_NEGATIVE_BUTTON_CAPTION, negativeButtonCaption);
        promptDialog.setArguments(args);
        return promptDialog;
    }

    ... implementation ...
}

This way dialog’s implementation details and constants aren’t visible to the outside code. External classes only know about that factory method.

By the way, I almost never use Parcelables in my code. If I need to pass some data structure as Fragment’s argument, I use standard Java serialization:

public class SomeDialog extends BaseDialog {

    protected static final String ARG_DATA = "ARG_DATA";

    public static SomeDialog newInstance(Data data) {
        SomeDialog someDialog = new SomeDialog();
        Bundle args = new Bundle(1);
        args.putSerializable(ARG_DATA, data);
        someDialog.setArguments(args);
        return someDialog;
    }

    ... implementation ...
}

Such a use of Serializable instead of Parcelable makes the code much simpler, but some developers don’t like it. I, personally, have been using it for years and never had any issues.

As I said, one of the main benefits of DialogFragment is the fact that it inherits full Fragment’s lifecycle. Therefore, you need to understand this lifecycle in details. I summarized my approach to Fragment’s lifecycle in this post and it applies to DialogFragment as well.

How to Show and Dismiss DialogFragment

Showing DialogFragment is pretty straightforward:

     SomeDialog.newInstance("someParameter").show(mFragmentManager, null);

The second argument to show() method is Fragment tag associated with the dialog. We’ll discuss it shortly.

Some dialogs contain the logic that dismisses them, but not all of them do. For example, if you show progress indication in a dialog when user initiates some async flow, you’ll want to dismiss it after the flow completes. Watch out for this mistake:

     mProgressDialog = ProgressDialog.newInstance();
     mProgressDialog.show(mFragmentManager, null);

and then in a different place in code:

     mProgressDialog.dismissAllowingStateLoss();

The above approach will result in NPE if the app undergoes configuration change or save & restore after the dialog is shown and before it’s dismissed. To prevent this problem, follow this simple rule: never keep references to DialogFragment instances.

Alright, but how do you dismiss that dialog if you can’t keep a reference to it? That’s where the aforementioned tag parameter comes in. First, assign unique tag to the dialog when you show it:

    ProgressDialog.newInstance().show(mFragmentManager, "progressDialog");

and then use the same tag to query for the instance of the dialog:

    Fragment progress = mFragmentManager.findFragmentByTag("progressDialog");
    if (progress != null) {
        ((DialogFragment) progress).dismissAllowingStateLoss();
    }

But what happens if there is already a dialog shown when you try to show a new one? Well, both of them will be visible at the same time. That’s not what you’d usually want to achieve. So, either you desing your app such that it won’t happen, or you add this check before you show a new dialog:

    List<Fragment> fragments = getSupportFragmentManager().getFragments();
    for (Fragment fragment : fragments) {
        if (DialogFragment.class.isAssignableFrom(fragment.getClass())) {
            ((DialogFragment)fragment).dismissAllowingStateLoss();
        }
    }
    ProgressDialog.newInstance().show(mFragmentManager, "progressDialog");

If you wonder why I use dismissAllowingStateLoss() instead of dismiss(), then you can read the post about Fragment lifecycle that I linked above.

Latest releases of support library and AndroidX added isStateSaved() to Fragment’s API, so this is probably the most correct way to dismiss DialogFragment:

    Fragment progress = getSupportFragmentManager().findFragmentByTag("progressDialog");
    if (progress != null) {
        if (progress.isStateSaved()) {
            ((DialogFragment) progress).dismissAllowingStateLoss();
        } else {
            ((DialogFragment) progress).dismiss();
        }
    }

At this point you can feel a bit uncomfortable with all these nuances and ceremonies. That’s totally alright and legitimate. Fragments and, especially, DialogFragments are so tricky that even experienced developers ocassioanlly screw them up.

DialogHelper

Managing DialogFragments in your app is boring and error-prone activity that involves lots of repeated code. Wouldn’t it be great to have a library that takes care of all of that? That’s what motivated me when I wrote DialogHelper – a small library that makes working with DialogFragments a breeze.

For example, the aforementioned use case of showing and then dismissing progress indication could be implemented in the following way with DialogHelper:

    mDialogHelper.showDialog(ProgressDialog.newInstance(), "progressDialog");

and then when you want to dismiss this specific dialog:

    if ("progressDialog".equals(mDialogHelper.getCurrentlyShownDialogId())) {
        mDialogHelper.dismissCurrentlyShownDialog();
    }

This looks much nicer, isn’t it?

DialogHelper makes sure that only one DialogFragment is visible and then dismisses it in the safest manner. It works across configuration changes and save & restore flow. And the only piece of information that you use to manage your dialogs are their unique dialog IDs.

Receiving Notifications from DialogFragment in Other Components

One surprisingly tricky question with respect to DialogFragments is: how to pass notifications from DialogFragment to other components? For example, if I have DialogFragments with two buttons, how can I notify the component that started it about user’s choice?

While implementing such notification schemes, developers often do the following mistake:

    DialogFragment someDialog = SomeDialog.newInstance("do something?");
    someDialog.setListener(buttonClicked -> {
        switch (buttonClicked) {
            case POSITIVE:
                // do something
                break;
            case NEGATIVE:
                // do something else
                break;
        }
    });
    mDialogHelper.showDialog(someDialog, "someDialog");

The problem are once again configuration change and save & restore flows. During these flows DialogFragment will be re-created, so it’ll lose the reference to the listener. This will lead to NPE in the best case, or silent malfunction and many frustrated users in the worst case.

To achieve this goal, I, personally, use event buses. Now, event buses are tricky beasts that look nice and innocent on the first sight, but can make your application very hard to maintain if used incorrectly or abused. I know all that from my personal experience, but in this specific case event buses constitute a very reasonable trade-off.

The best event bus for Android is Green Robot’s EventBus library. Using this library, I can post event notifications in specific dialogs:

    protected void onPositiveButtonClicked() {
        dismissAllowingStateLoss();
        mEventBus.post(
                new PromptDialogDismissedEvent(
                        mDialogHelper.getDialogId(this),
                        PromptDialogDismissedEvent.ClickedButton.POSITIVE
                )
        );
    }
    
    protected void onNegativeButtonClicked() {
        dismissAllowingStateLoss();
        mEventBus.post(
                new PromptDialogDismissedEvent(
                        mDialogHelper.getDialogId(this), 
                        PromptDialogDismissedEvent.ClickedButton.NEGATIVE
                )
        );
    }

and then subscribe to specific events in the components which are interested in them:

    @Override
    protected void onStart() {
        super.onStart();
        mEventBus.register(this);
    }

    @Override
    protected void onStop() {
        super.onStop();
        mEventBus.unregister(this);
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEvent(PromptDialogDismissedEvent event) {
        switch (event.getDialogId()) {
            case DIALOG_ID_PROMPT:
                String clickedButton =
                        event.getClickedButton() == PromptDialogDismissedEvent.ClickedButton.POSITIVE ? "positive" : "negative";
                Toast.makeText(this, "Prompt dialog dismissed with " + clickedButton + " button click", Toast.LENGTH_LONG).show();
                break;
        }
    }

There is one major issue with this implementation though: you can accidentally erase onEvent(PromptDialogDismissedEvent) method and not notice that. The application will compile and you won’t get any runtime errors, but it won’t work as expected. That’s a risk.

The root cause of this risk is that the coupling between the dialog and the component that receives the event isn’t strong enough. You might be surprised by this statement because it’s often stated that coupling is bad and you always want to have the least possible coupling between components in your system. Well, it’s not exactly the case and this wrong mindset is exactly what leads to maintainability problems if you rely on event buses too much. What you want to have is an optimal amount of coupling.

The good news are that this risk can be mitigated. The bad news are that the mitigation strategy is to unit test your code, which is, unfortunately, relatively uncommon practice in Android world.

If you have unit test that ensures existense of this method, you won’t be able to “lose” it accidentally during further development and maintainance. However, there is a bit of trickiness involved in unit testing dialogs, so let’s discuss that.

Unit Testing Dialog Flows

If you’re using architectural pattern like MVC and have standalone controllers which are covered with unit tests, you can’t put the following code that shows a dialog in there (or any other unit tested component for that matter):

    mDialogHelper.showDialog(ProgressDialog.newInstance(), "progressDialog");

Static call to dialog’s factory method will make the controllers non-testable. To work around this issue, you can introduce DialogsManager abstraction:

    public class DialogsManager {

        private final DialogHelper mDialogHelper;

        public DialogsManager(DialogHelper dialogHelper) {
            mDialogHelper = dialogHelper;
        }

        public void showProgressDialog(String dialogId) {
   	    mDialogHelper.showDialog(ProgressDialog.newInstance(), dialogId);	
        }
    }

By using this abstraction inside your controllers you make them unit testable and also hide unrelated implementation details from them.

Conclusion

In this article I tried to cover the most important aspects of working with dialogs and, specifically, DialogFragment in Android. It took me years of trial and error to finally settle on a specific approach towards dialogs, and I hope that my experience will spare you some time.

If you’re using DialogFragments in your app, I highly recommend to give DialogHelper library a try. Much of the hacky spaghetti code related to dialogs in Android that I’ve seen could be prevented with this small and humble utility.

Last, if you want to see some more advanced techniques related to DialogFragment that I’m using, I invite you to review the source code of DialogHelper’s sample application. In particular, I’m using a special approach that supports programmatically defined animations of dialogs (as opposed to XML). I can’t recommend this approach as a general solution because it’s quite complex, but if you need to animate the hell out of your dialogs and XML animations don’t suffice – you might like it.

As usual, leave your comments and questions below and don’t forget to subscribe to posts notifications if you liked this article.

Check out my advanced Android development courses on Udemy

Subscribe for new posts!

Leave A Comment