14-Feb-11 (Created: 14-Feb-11) | More in 'Older Android Notes'

07 Working with AlertDialogs

Alert Dialogs are common for validating forms. Consider the following example


if (validate(field1) == false)
{
   //indicate that formatting is not valid through an alert dialog
   showAlert("What you have entered in field1 doesn't match required format");
   //set focus to the field
   //..and continue
}

This is frequently done in javascript through "alert" function which looks like


alert("Hello! this is an alert")

And javascript will pop open a synchronous dialog that shows this message to the user and displays an OK button that would be pressed by the user. On return the flow of the program continues.

Support for alert dialogs in Android

As it turns out there is no such direct function called "showAlert" that is readily available. Instead Android supports a general purpose facility for working with a number of alert dialogs. These features are made available through an alert dialog builder as defined by the class:


android.app.AlertDialog.Builder

The supported set of alert dialogs include


Yes/No message dialog
Yes/No Long Message Dialog
Pick One from a List Dialog
Pick a number of items from a larger set
Progress Dialog
Single choice from a set of choices dialog
A prompt dialog

The approach to build any of these dialogs is similar.

  • Construct a Builder Object
  • Set a number of parameters such as number of buttons, list of items etc
  • Tell the Builder to build the dialong
  • Depending on what is set on the builder object an appropriate dialog is built

Here is how this approach is used to create a general purpose "showAlert" like dialog.


public class Alerts
{
   public static void showAlert(String message, Context ctx)
   {
      //get a builder and set the view
      AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
      builder.setTitle("Alert Window");
      
      //add buttons and listener
      PromptListener pl = new EmptyListener();
      builder.setPositiveButton("OK", pl);
      
      //get the dialog
      AlertDialog ad = builder.create();
      
      //show
      ad.show();
   }
}

public class EmptyListener 
implements android.content.DialogInterface.OnClickListener
{
   public void onClick(DialogInterface v, int buttonId)
   {
   }
}

Encouraged by this success let's see if we can take on another staple dialog in javascript called the "prompt" dialog where the user is prompted with a hint and the user enters some text into an edit box. The prompt dialog will return that string to the program to continue.

This is a good example because it features a number of facilities provided by the Builder class. So let us consider the code where we build up the prompt dialog and show. Once complete, the intention is to use code something similar to this:


public void OnDeleteFile()
{
   String filename = Alerts.prompt("Enter filename string", this);
   if (filename == null) {
      //cancel has been clicked
      //nothing to do
      //return
   }
   else {
      //filename is available
      //
      deletefile(filename);
   }
}

So with no further ado, let's implement the "prompt" static function


public class Alerts
{
   public static String prompt(String message, Context ctx)
   {
      //load some kind of a view
      LayoutInflater li = LayoutInflater.from(ctx);
      View view = li.inflate(R.layout.promptdialog, null);
      
      //get a builder and set the view
      AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
      builder.setTitle("Prompt");
      builder.setView(view);
      
      //add buttons and listener
      PromptListener pl = new PromptListener(view,ctx);
      builder.setPositiveButton("OK", pl);
      builder.setNegativeButton("Cancel", pl);
      
      //get the dialog
      AlertDialog ad = builder.create();
      
      //show
      ad.show();
      
      return pl.prompt;
   }
}

Now let us consider how the listener is designed to make this happen


public class PromptListener 
implements android.content.DialogInterface.OnClickListener
{
   String promptValue = null;
   View promptDialogView = null;
   public PromptListener(View inDialogView)
   {
         promptDialogView = inDialogView;
   }
   public void onClick(DialogInterface v, int buttonId)
   {
         if (buttonId == DialogInterface.BUTTON1)
      {
         //ok button
      promptValue = getPromptText();
      }
      else
      {
         //cancel button
         promptValue = null;
      }
   }
   private String getPromptText()
   {
         EditText et = 
      (EditText)
      promptDialogView.findViewById(R.id.promptEditTextControlId);
      
      return et.getText().toString();
   }
}

To understand the views used by this prompt dialog let us take a look at the view XML definition required by the following code


      LayoutInflater li = LayoutInflater.from(ctx);
      View view = li.inflate(R.layout.promptdialog, null);

The corresponding xml file will be


\res\layout\promptdialog.xml

Contents of that file would be


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView 
        android:id="@+id/prompt_message"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:layout_marginLeft="20dip"
        android:layout_marginRight="20dip"
        android:text="@string/alert_dialog_username"
        android:gravity="left"
        android:textAppearance="?android:attr/textAppearanceMedium" />
            
    <EditText
        android:id="@+id/prompt_edit_text_control_id"
        android:layout_height="wrap_content"
        android:layout_width="fill_parent"
        android:layout_marginLeft="20dip"
        android:layout_marginRight="20dip"
        android:scrollHorizontally="true"
        android:autoText="false"
        android:capitalize="none"
        android:gravity="fill_horizontal"
        android:textAppearance="?android:attr/textAppearanceMedium" />
</LinearLayout>

However after writing all this code you will notice that the "prompt" dialog always returns null even if the user is entering text into the prompt dialog. As it turns out, in the following code


ad.show() //dialog.show
return pl.getPrompt(); // listener.getprompt

The show will invoke the dialog asynchronously. This means the "promptListener" gets called for the prompt value before user had time to enter and click OK.

I have taken you on this false ride (that I took my self early on) to demonstrate you an important limitation of the architecture of dialogs in Android. With that introduction to the oddity of Android dialogs, let us look into the philosophy behind "asynchronous" modal dialogs in Android.

What you must know about dialogs in Android

Displaying or showing dialogs in Android is an asynchronous process. Once a dialog is shown the main thread that invoked the dialog returns and continues to process the rest of the code. Although "modality" relationship between the activity that invoked the dialog and the dialog itself is maintained, the parent activity is forced to return to its message pump.

The "modality" is maintained because the focus is drawn to the dialog box and user won't be able to click on the parent activity while the dialog is on top.

The semantics of "modal" dialogs in some windowing systems suggest that user must provide a response before the parent window can continue. The parent thread appears to be blocking for the caller. Android supports the former and not the later. Android seemed to have taken this route because the designers of Android wanted the parent window or activity to continue to process other non-ui events such as life cycle events. If the thread were to block this would not be possible.

The implication of this model is that you cannot have a simple dialog where you can ask for a response and wait for it before moving on. In fact your programming model for dialogs have to be completely different basing on call backs.

Rearchitecting the "prompt" dialog for reuse

So you can't have dialogs that return stuff to the caller directly. Given that "truth" how can we implement something like a "prompt" dialog functionality in a reasonable way. This can only be done with call backs. What follows is an implementation that tries to stay as open and as generic as possible so that one can use it for any number of prompts from the same activity and at the same time allows multiple actions to be invoked for each prompt.

Let us start by examining how an activity that makes use of prompt dialogs could look like


public Class MyActivity extends Activity
implements IStringPromptCallBack
{

   public static int PROMPT_ACTION_1;
   public static int PROMPT_ACTION_2;
   
   public void exercisePrompt()
   {
      Alert.prompt(this, //callback for the dialog
         ,"Please enter data for xyz.." // message to prompt
         ,this   //This context for dialog builder
         ,PROMPT_ACTION_1 ) // to distinguish between multiple prompts
   }
   
   //callbacks allowing multiple callback actions
   public void promptCallBack(String promptedString, 
         int buttonPressed, //which button was pressed 
         int actionId) //To support multiple prompts
   {
      if (buttonPressed == IStringPrompterCallBack.OK_BUTTON)
      {
         ..do something with promptedString
      }
      else
      {
         ..deal with cancel
      }
   }
}

Let's look at how IStringPromptCallBack looks like


public interface IStringPrompterCallBack 
{
   public static int OK_BUTTON = -1;
   public static int CANCEL_BUTTON = -2;
   public void promptCallBack(String promptedString, 
         int buttonPressed, //which button was pressed 
         int actionId); //To support multiple prompts
}

Given this let's redesign the "prompt" method on Alerts static class that was introduced earlier


public class Alert
{
    public static void Prompt(IStringPrompterCallBack cb
          ,String message 
          ,Context ctx
          ,int actionId)
    {
       LayoutInflater li = LayoutInflater.from(ctx);
       View view = li.inflate(R.layout.promptdialog, null);
       AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
       builder.setTitle("Prompt");
       builder.setView(view);
       PromptListener pl = new PromptListener(view,ctx,cb,actionId);
       builder.setPositiveButton("OK", pl);
       builder.setNegativeButton("Cancel", pl);
       AlertDialog ad = builder.create();
       ad.show();
    }
}

Notice how the "prompt" method is now a "void" method and returns nothing. The implementation will become clear by noticing what the PromptListener implemenation will look like:


public class PromptListener 
implements android.content.DialogInterface.OnClickListener
{
   private IStringPrompterCallBack cb = null;
   private int actionId = 0;
   private View promptView;
   public PromptListener(View view
         , Context ctx
         , IStringPrompterCallBack inCb
         , int inActionId)
   {
      promptView = view;
      cb = inCb;
      actionId = inActionId;
   }
   public void onClick(DialogInterface v, int buttonId)
   {
      if (buttonId == DialogInterface.BUTTON1)
      {
         //ok button
         String promptValue = getEnteredText();
         cb.promptCallBack(promptValue 
               ,IStringPrompterCallBack.OK_BUTTON
               ,actionId);
      }
      else
      {
         String promptValue = getEnteredText();
         cb.promptCallBack(null 
               ,IStringPrompterCallBack.CANCEL_BUTTON
               ,actionId);
      }
   }
   private String getEnteredText()   
   {
      EditText et = 
         (EditText)
         promptView.findViewById(R.id.editText_prompt);
      String enteredText = et.getText().toString();
      Log.d("xx",enteredText);
      return enteredText;
   }
}

Managed Dialogs: The recommended way

Although it is reasonable to design dialogs this way, Android recommends a considerably different approach for implementing dialogs. In the recommended approach, Android discourages creating new dialogs in response to actions instead would like to reuse dialog instances that were created before. Android follows a managed protocol to make this scheme work.

Let me describe the protocol first before giving somne examples.


MyActivity
{
   //Give unique ids for each type of dialog 
   //that you may want to use
   public static int DIALOG_1=1;
   ..
   
   @override
   protected Dialog onCreateDialog(int dialogId){
      switch(dialogId) {
      case DIALOG_1:
         createDialog1();
      }
   }
   Dialog createDialog1()
   {
      //use Builder to create the dialog you want
      //register call backs
   }
   private void respondToSomeAction()
   {
      //asynchronous call
      showDialog(DIALOG_1);
   }
   private void someCallBackFunctions()
   {
      //have the dialog call you back
   }
   
    @Override
    protected void onPrepareDialog(int id, Dialog dialog) {
        switch (id) {
            case DIALOG_1:
            //..set any dynamic values 
        }
    }    
}//eof-class

To understand the above protocol lets' start with the following code


   private void respondToSomeAction()
   {
      //asynchronous call
      showDialog(DIALOG_1);
   }

This part of code is calling showDialog with an id in response to some action in the activity class. The action could be a menu action or a button click or any other. As part of this method you need to tell Android a number indicating a dialog type. Android will take this number and see if the dialog identified by this number is already in cache. If it is available it will call the onPrepareDialog for this dialog and then shows the dialog in an asynchronous thread. If the dialog doesn't exist it will call the "onCreateDialog()" with the dialog id passed. It is upto the "onCreateDialog()" function to take that id and create an appropriate dialog. Once this dialog is created by onCreateDialog it will be cached and onCreateDialog won't be used for this id again for the life of that activity.

As already have been emphasized, due to the asynchronous nature of the dialogs the activity should also reserve some methods for call backs to handle when the dialog is dismissed due to any of its buttons being clicked.

Reimplementing the alert dialog as a managed dialog

Let us take the protocol above and see if we can reimplement the previous non managed alert dialog as a managed alert dialog. Let us start by defining a unique ID for this dialog in the context of a given activity


    //unique dialog id
   private static final int DIALOG_ALERT_ID = 1;
   //a counter to demostrate onPrepareDialog
   private int alertDemoCounter=0;

In OnCreateDialog make a call to create the alert dialog.


    @Override
    protected Dialog onCreateDialog(int id) {
        switch (id) {
            case DIALOG_ALERT_ID: 
               return createAlertDialog();
        }
        return null;
    }
   
    //set this 
    private String dynamicMessage = "Initial Message";
    private Dialog createAlertDialog()
    {
       AlertDialog.Builder builder = new AlertDialog.Builder(this);
       builder.setTitle("Alert");
       builder.setMessage(dynamicMessage);
       EmptyOnClickListener l = new EmptyOnClickListener();
       builder.setPositiveButton("Ok", l );
       AlertDialog ad = builder.create();
       return ad;
    }

The createALertDialog uses the previou empty on click listener. Notice how the message string is named a "dynamicMessage" string to demonstrate that every time we display a message it is a new message. To accomodate this, code has a private variable called "alertDemoCounter".

The following code uses the onPrepareDialog to set this dynamic message in the alert dialog.


    @Override
    protected void onPrepareDialog(int id, Dialog dialog) {
        switch (id) {
        case DIALOG_ALERT_ID: 
           prepareAlertDialog(dialog);
        }
    }
    
    private void prepareAlertDialog(Dialog d)
    {
       AlertDialog ad = (AlertDialog)d;
       ad.setMessage(dynamicMessage);
    }

Now that we have set the dialog up, we can proceed to use it. Because the dialog is a managed dialog, we have to set the value of the dynamic string every time this dialog is called.


    private void exerciseAlertDialog()
    {
       this.dynamicMessage="Dynamic Message:" + alertDemoCounter++;
       showDialog(this.DIALOG_ALERT_ID);
    }

Simplifying the dialog protocol

If you have noticed working with these alert dialogs can become quite messy and polutes the main line of code. It is possible to abstract out this protocol in an abstraction that would allows us the following


    @Override
    protected Dialog onCreateDialog(int id)
   {
        return this.dialogRegistry.create(id);
   }
         
    @Override
    protected void onPrepareDialog(int id, Dialog dialog) 
   {
        this.dialogRegistry.prepare(dialog, id);
   }

I am going to rewrite the alert dialog one more time with this new abstraction. Under this new abstraction I only need to add the following code to the above to make it work:


   //Get an ID for your dialog
   private static final int DIALOG_ALERT_ID_3 = 3;
   
    //Create a DialogRegistry to hold dialog builders
    private DialogRegistry dr = new DialogRegistry();
   
   //Create your dialog outside of the code
   private GenericManagedAlertDialog gmad = 
      new GenericManagedAlertDialog(this,"InitialValue");
    
   //Register your dialogs with registry in
   //activity creation
    private void registerDialogs()
    {
       dr.registerDialog(this.DIALOG_ALERT_ID_3, gmad);
    }
   
   //Show the dialog with prepare capability
    private void exerciseDialogRegistry()
    {
       gmad.setAlertMessage("some message:" + this.alertDemoCounter++);
       showDialog(this.DIALOG_ALERT_ID_3);
    }

The magic becomes clear when you see the code for the following classes


public interface IDialogProtocol 
{
   public Dialog create();
   public void prepare(Dialog dialog);
}

public class GenericManagedAlertDialog implements IDialogProtocol
{
   private String alertMessage = null;
   private Context ctx = null;
   public GenericManagedAlertDialog(Context inCtx, String initialMessage)
   {
      alertMessage = initialMessage;
      ctx = inCtx;
   }
   public Dialog create()
   {
       AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
       builder.setTitle("Alert");
       builder.setMessage(alertMessage);
       EmptyOnClickListener l = new EmptyOnClickListener();
       builder.setPositiveButton("Ok", l );
       AlertDialog ad = builder.create();
       return ad;
   }
   
   public void prepare(Dialog dialog)
   {
       AlertDialog ad = (AlertDialog)dialog;
       ad.setMessage(alertMessage);
   }
   public void setAlertMessage(String inAlertMessage)
   {
      alertMessage = inAlertMessage;
   }
}

public class DialogRegistry 
{
   SparseArray idsToDialogs
   = new SparseArray();
   public void registerDialog(int id, IDialogProtocol dialog)
   {
      idsToDialogs.put(id,dialog);
   }
   
   public Dialog create(int id)
   {
      IDialogProtocol dp = idsToDialogs.get(id);
      if (dp == null) return null;
      
      return dp.create();
   }
   public void prepare(Dialog dialog, int id)
   {
      IDialogProtocol dp = idsToDialogs.get(id);
      dp.prepare(dialog);
   }
}

References