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:

“Create new playlist or add to existing” AlertDialog An AlertDialog for entering a new playlist name, or A ProgressDialog shown when loading a user’s list of playlists, and, An AlertDialog with single-choice items for picking a playlist from the user’s list of playlists A ProgressDialog shown when creating a new playlist and saving a song to this playlist, or A ProgressDialog shown when adding a song to an existing playlist An AlertDialog shown when creating a new playlist fails, or 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

creation and display code Encapsulates all relevant AsyncTask code

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!