androiddevblog

An Android developer at Grooveshark

Sharing complex Dialog interactions across multiple Activities

with 8 comments

One of the more common problems that I have run into in Android application development is how to reuse complex, multi-step dialog actions across multiple activities while avoiding code duplication.

In the Grooveshark Android app, several instances of Activity and ListActivity give the user the option to add a song to a playlist. Choosing to add to a playlist starts a multi-step series of Dialog actions: the user is asked if they want to add the song to an existing playlist, or to create a new playlist. If the user chooses to add to an existing playlist, they’re presented with a radio-list of their playlists, otherwise they’re asked to enter a name for their new playlist. After entering a name and pressing save, a ProgressDialog is shown while a network request is made to create the playlist through Grooveshark’s API. If the user chooses to add a song to an existing playlist, a network request is then made to the Grooveshark API to add the song to the playlist, spawning a ProgressDialog.

As a rule, when making any network requests from the app, a “cancelable” indeterminate ProgressDialog must be shown. If the user cancels the operation by pressing their phone’s “back” hard-key, the network request should be canceled immediately, and any resources freed. Additionally, all dialogs must respond to application configuration changes—like an orientation change—and any ongoing network requests must survive these configuration changes, too.

This “add to playlist” interaction requires eight dialogs:

  1. “Create new playlist or add to existing” AlertDialog
  2. An AlertDialog for entering a new playlist name, or
  3. A ProgressDialog shown when loading a user’s list of playlists, and,
  4. An AlertDialog with single-choice items for picking a playlist from the user’s list of playlists
  5. A ProgressDialog shown when creating a new playlist and saving a song to this playlist, or
  6. A ProgressDialog shown when adding a song to an existing playlist
  7. An AlertDialog shown when creating a new playlist fails, or
  8. An AlertDialog shown when adding a song to an existing playlist fails

This process uses three separate AsyncTask instances:

  • Loading the logged-in user’s complete list of playlist names either from a network request or from the local database
  • Creating a new playlist and adding a song to it
  • Adding a song to an existing playlist

This is a complex set of interactions that totals around 600 lines of code, and this set of interactions needs to be launched from nearly every Activity in the Grooveshark application.

Most of these activities are instances of ListActivity—the user’s list of favorite songs, popular songs, cached songs, playlist songs, song search results, etc.—so this Dialog and AsyncTask code could live in a base ListActivity class from which all other list activities derive. The Grooveshark application does make use of a base ListActivity class that provides a common context and option menu, but not all activities needing the “Add to Playlist” feature are instances of ListActivity but not all activities needing the “Add to Playlist” feature are instances of ListActivity;  “NowPlaying”—our player and queue Activity—is a notable example.

Composition could be used to give each Activity needing “Add to Playlist” functionality these dialogs and AsyncTask instances, but this would require a litany of cumbersome callbacks to connect the host activity’s lifecycle callbacks to our proxy object—callbacks like onCreateDialog() to create our eight dialogs, and onRetainNonConfigurationInstance() to keep a reference to any active AsyncTask instances across Activity configuration changes.

My ideal solution to this problem:

  • Does not unnecessarily duplicate code
  • Does not use object inheritance
  • Does not use object composition
  • Encapsulates all Dialog creation and display code
  • Encapsulates all relevant AsyncTask code
  • Can be launched from any application Activity
  • Can respond to all Activity lifecycle methods

The solution I’ve found that fulfills all of our requirements uses a single transparent Activity instance to handle all related Dialog steps. This host Activity contains all Dialog code in onCreateDialog()—and onPrepareDialog(), if necessary—and encapsulates all of its network requests and database interactions in member AsyncTask instances. I call this the “transparent dialog-host activity” pattern, and the Grooveshark app uses this for its “Add to Playlist” feature, and in other places.

Transparent Dialog-Host Activity

I’ve put together a demo application to demonstrate how to use this pattern. It’s a simple to-do list app that let’s you create and edit to-do items. The demo app has two ListActivity instances and one transparent Activity instance that hosts its dialogs. We’ll walk through the important parts, and you can grab the entire source from the GitHub repo.

Getting Started

First create the Activity to contain your Dialog instances. In the demo application I linked to, this is the DialogTasks.java file. All dialogs are activity-managed, which means that on configuration change, Android handles dismissing and reopening any dialogs that were open before the configuration changed occurred.

It is always best to use managed dialogs; do not create and show dialogs on your own, always use onCreateDialog(), onPrepareDialog(), showDialog() and dismissDialog() or removeDialog(). Showing and dismissing dialogs without knowledge of and the ability to respond to lifecycle and configuration changes can be extremely error-prone and will crash your entire application if you do it wrong.

In DialogTasks.java we first define our dialog id constants:

private static final int EDIT_NAME_DIALOG = 0;
private static final int EDIT_DATE_DIALOG = 1;
private static final int CREATE_NEW_DIALOG = 2;
private static final int SAVING_DIALOG = 3;
private static final int DATE_PICKER_DIALOG = 4;
private static final int CONFIRM_DELETE_DIALOG = 5;
private static final int DELETING_DIALOG = 6;

And we define our dialogs in our activity’s onCreateDialog() method. Our demo program uses seven dialogs: a DatePickerDialog for choosing your to-do item’s due date, four AlertDialog instances for collecting input and showing a confirmation prompt, and two ProgressDialog instances that we show when saving changes.

/**
 * Lifecycle method invoked to create a dialog shown via showDialog()
 * NOTE: we override the older, deprecated method so that we can work
 * on older sdk versions.
 */
@Override
public Dialog onCreateDialog(int which)
{
    if (which == EDIT_NAME_DIALOG) {
        final View dialogView = getLayoutInflater().inflate(R.layout.new_task_dialog, null);
        AlertDialog dialog = new AlertDialog.Builder(this)
            .setView(dialogView)
            .setTitle(R.string.edit_task_title)
            .setPositiveButton(R.string.save, new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int id)
                {
                    String name = ((EditText) dialogView.findViewById(R.id.task_name_edit)).getText().toString();
                    ToDoList.ToDoItem toDoItem = new ToDoList.ToDoItem(name,
                            currentToDoItem.year,
                            currentToDoItem.monthOfYear,
                            currentToDoItem.dayOfMonth);
                    ToDoList.getInstance().remove(currentToDoItem);
                    ToDoList.getInstance().add(editedToDoItem);
                    showDialog(SAVING_DIALOG);
                    // Normally this would start an AsyncTask to make a network request or
                    // to write to a local database, but for this demo I'm using a Handler
                    // on the main thread to simulate the delay of sending a network request
                    // and waiting for its response.
                    handler.postDelayed(new Runnable() {
                        @Override
                        public void run()
                        {
                            Toast.makeText(DialogTasks.this, R.string.task_updated,
                                    Toast.LENGTH_SHORT).show();
                            setResult(RESULT_OK);
                            finish();
                        }
                    }, 3000);
                }
            })
            .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int id)
                {
                    setResult(RESULT_CANCELED);
                    finish();
                }
            }).create();
        return dialog;
    }
}

Rather than post the entire onCreateDialog() implementation from the demo, which is a bit lengthy, I’ve only posted an excerpt so that I can point out a few key things. Note that for both the “save” and “cancel” buttons, in their on-click listeners we set an activity result code via setResult() and we call finish() to end our containing Activity. DialogTasks.java can be launched with startActivityForResult(), as it is from our demo’s ToDoListActivity.java, and in this instance, the result code from DialogTasks.java is used to update its list.

It’s important to finish() your dialog activity from any “last step” of your dialog actions. If you do not invoke finish() when the last Dialog closes, a user of your application will be able to see whatever activity instance was open before the dialog activity launched, but they will not be able to interact with it–it will not receive any touch or menu events until the phone’s “back” hard-key is pressed.

DialogTasks.java supports four work-flows: creating a new to-do item, editing its name, editing its due date, and deleting a to-do item. To pick which work-flow, or task, to start, we use extras in the Intent used to start our activity.

These Intent extras are defined in DialogTasks.java

/**
 * Extra sent to create a new ToDo item
 */
public static final String CREATE_TODO_EXTRA = "createToDo";
/**
 * Extra sent to edit an existing ToDo item's name
 */
 public static final String EDIT_TODO_NAME_EXTRA = "editToDoName";
/**
 * Extra sent to delete an existing ToDo item
 */
public static final String DELETE_TODO = "deleteToDo";

And DialogTasks.java can be launched from any other Activity like so:

Intent i = new Intent(this, DialogTasks.class);
i.putExtra(DialogTasks.CREATE_TODO_EXTRA, true);
i.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(i);

Note that when starting our dialog-host activity, we disable Android’s transition animation with the Intent.FLAG_ACTIVITY_NO_ANIMATION flag. Without disabling the transition animation, the first Dialog opened from our transparent Activity would appear to open with an activity’s animation rather than the usual Dialog animation. On stock Android 2.x—API level 5—the dialog would appear as it slid-in from the right. This flag is only available on API levels 5 and higher, but luckily on stock Android versions prior to API level 5, the Activity transition animation is the same zoom-in effect used when opening activities, so the extra animation isn’t noticeable.

In DialogTasks.java we check which extras are set, and start the specified work-flow by displaying the first dialog of the task.

@Override
public void onCreate(Bundle inState)
{
    super.onCreate(inState);

    Bundle extras = getIntent().getExtras();
    if (extras != null) {
        currentToDoItem = extras.getParcelable(ToDoList.ToDoItem.TODO_EXTRA);
        if (extras.containsKey(CREATE_TODO_EXTRA)) {
            showDialog(CREATE_NEW_DIALOG);
        } else if (extras.containsKey(EDIT_TODO_NAME_EXTRA)) {
            showDialog(EDIT_NAME_DIALOG);
        } else if (extras.containsKey(DELETE_TODO)
                && extras.containsKey(ToDoList.ToDoItem.TODO_EXTRA)) {
            showDialog(CONFIRM_DELETE_DIALOG);
        } else {
            finish();
        }
        /*
         * Overwrite our Intent's original extras so that on configuration
         * change we do not create duplicate, overlapping dialogs. Recall that
         * on configuration change, Android will re-create and show any
         * already-open managed dialogs.
         */
        Intent intent = getIntent();
        if (intent != null) {
            intent.replaceExtras((Bundle) null);
        }
    }
}

Note that we replace our activity’s getIntent() extras with an empty Bundle. As mentioned in the source code comments, it’s important to replace these extras because on configuration change, the original Intent is re-delivered to our new Activity instance.

Recall that when using activity-managed dialogs, Android handles removing and re-displaying any open Dialog instances on configuration change. If we do not replace our activity’s Intent extras, when this event occurrs, overlapping dialogs will be shown: both the initial dialog opened in onCreate() by one of our Intent extras, and the re-displayed Dialog that was previously closed by Android.

Finally we make our Activity transparent and and remove its title bar by specifying styling options in AndroidManifest.xml.

<activity android:name="DialogTasks"
          android:theme="@android:style/Theme.Translucent.NoTitleBar"
          android:noHistory="true" />

A Caveat

So far the only thing that doesn’t work as you would expect it to when displaying dialogs in this way is that the underlying activity’s options menu is not shown when the phone’s “menu” hard-key is pressed. This is because the foreground, transparent Activity has focus, so all key events are delivered to it. This hasn’t yet been a problem in the Grooveshark app.

When to use this pattern

Any time you have a set of dialogs that are part of a “wizard-like” process–moving from step 1.) to step 2.), collecting user input and showing different dialogs in response to user input—if this functionality is needed from multiple Activity instances, it’s a good place to use transparent dialog-host activities. If you only need to show these dialogs from a single place in your app, this pattern isn’t a good fit. Additionally if you need to display only a single Dialog from multiple Activity instances, you should bite the bullet and define all of your Dialog code from within each individual activity’s onCreateDialog() method.

Don’t forget to grab the source code and have fun!

About these ads

Written by Skyler Slade

February 9, 2011 at 2:31 am

Posted in Demos

8 Responses

Subscribe to comments with RSS.

  1. “Any time you have a set of dialogs that are part of a “wizard-like” process–moving from step 1.) to step 2.)… it’s a good place to use transparent dialog-host activities.” — any reason why you just didn’t create a wizard?

    Mark Murphy

    February 9, 2011 at 1:01 pm

    • Hi Mark! I remember stumbling across something in the SDK once that was described a being useful for creating a wizard-like process. When I searched later, I couldn’t find anything. Are you referring to a ViewFlipper? Or something else?

      Skyler Slade

      February 9, 2011 at 10:46 pm

      • ViewFlipper would be a fairly easy solution — it would hold the wizard pane contents, with the next/previous step buttons below the flipper. Creating an actual WizardView or WizardActivity is one of the 18,000 items on my to-do list, though somebody may have beaten me to it, as I haven’t gone looking recently.

        Mark Murphy

        February 10, 2011 at 7:05 am

  2. Keep in mind you can create an Activity and set it’s style to “@android:style/Theme.Dialog” in the manifest, and it will look like a dialog instead of a full screen activity. I think, as mentioned, a ViewFlipper or something similar would make this a little simpler to implement.

    On a side note, I think indeterminate progress dialogs are one of the worst UI experiences — at least for mobile devices. In most cases, whenever I come across one, I cringe, and often just close the app altogether. I think a more pleasant user experience is to assume the operation succeeded, start loading content or whatever seems natural, and you can alert the user later if it ultimately fails, or cache the update to attempt it again later. Maybe show a spinner somewhere on the activity. For example, look at how GMail handles this. It does show a “Sending message” dialog that blocks while it’s sending — it shows a toast and sends the message in the background.

    Joe

    February 11, 2011 at 10:32 am

    • Joe, thanks for your comment! Styling an Activity as a Dialog is extremely handy and I’ve used this before. But I chose not to do this for Grooveshark’s “Add to Playlist” feature because I wanted to contain all dialog and view code for this feature within a single Activity. It was easier and more manageable for me to keep all dialogs and AsyncTasks together, in the same file.

      While I generally agree that indeterminate progress dialogs can create a bad experience, there are some instances where to create a better experience, I feel that they are necessary; as they are in the Grooveshark app’s “Add to Playlist” feature that I used as an example.

      When choosing to add a song to an existing playlist, if a user hasn’t already viewed his list of playlists, we lazy-load these when he chooses to create a new one. In this instance we show an indeterminate progress dialog while we wait for this list to load, before we show a multi-choice selector.

      When making simple changes—like adding a song as a favorite—we do this in the background, and assume success, only showing a failure notice on failure, as you’ve outlined. However, in the case of creating a new playlist, the server could return an error because the chosen playlist name was not unique.

      If for example, since the app last loaded the user’s list of playlists, the user had created a new playlist on our web site with the same name, we would receive a “playlist name isn’t unique” error from the server.

      If we were to completely handle creating a new playlist in the background, and this were to fail, we’d need to prompt the user to select a new, unique, playlist name. And I feel that if this error and prompt were shown removed from the initial process of creating a new playlist, it would make for a jarring experience.

      So while I agree that indeterminate progress dialogs can make for a poor experience, and that it is often better to handle such operations in the background and assume success, I think there are some instances where it makes for a better experience to block with an indeterminate progress dialog and wait for a success response before continuing.

      Skyler Slade

      February 14, 2011 at 12:04 am

  3. Really it’s a nice post! Very useful explanation. Thank you. Can you please post us few more useful Complex scenarios we face in real time like this..

    Sivaram

    September 27, 2011 at 12:20 pm

    • Thanks for this post, i’m glad to have found it. I recently ran into a problem of keeping my AlertDialog “modal”; although there are plenty of journals discussing why we should not do it, i still find this topic quite confusing. If you would blog on this, I’m sure many of us will enjoy it…

      HT

      August 2, 2012 at 1:24 pm

  4. [...] http://androiddevblog.wordpress.com/2011/02/09/sharing-complex-dialog-interactions-across-multiple-a… KategorienAndroid Tags: Kommentare (0) Trackbacks (0) Einen Kommentar schreiben Trackback [...]


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: