Android "Best Practice" returning values from a dialog

Android

Android Problem Overview


What is the "correct" way to return the values to the calling activity from a complex custom dialog - say, text fields, date or time picker, a bunch of radio buttons, etc, plus a "Save" and "Cancel" button?

Some of the techniques I've seen on the web include:

  • public data members in the Dialog-derived class which can be read by the Activity

  • public "get" accessors . . . " . . " . . "

  • Launching the dialog with an Intent (as opposed to show() ) plus handlers in the Dialog class which take input from the various controls and bundle them up to be passed back to the Activity so when listener hits "Save" the bundle is passed back using ReturnIntent()

  • Listeners in the Activity which process input from the controls that are in the dialog e.g., so the TimePicker or DatePicker's listeners are really in the Activity. In this scheme practically all the work is done in the Activity

  • One Listener in the Activity for the "Save" button and then the Activity directly interrogates the controls in the dialog; the Activity dismisses the dialog.

...plus more that I've already forgotten.

Is there a particular technique that's considered the canonically correct or "best practice" method?

Android Solutions


Solution 1 - Android

Perhaps I'm mis-understanding your question, but why not just use the built in listener system:

builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
    public void onClick(DialogInterface dialog, int id) {
        // run whatever code you want to run here
        // if you need to pass data back, just call a function in your
        // activity and pass it some parameters
    }
})

This is how I've always handled data from dialog boxes.

EDIT: Let me give you a more concrete example which will better answer your question. I'm going to steal some sample code from this page, which you should read:

http://developer.android.com/guide/topics/ui/dialogs.html

// Alert Dialog code (mostly copied from the Android docs
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Pick a color");
builder.setItems(items, new DialogInterface.OnClickListener() {
    public void onClick(DialogInterface dialog, int item) {
        myFunction(item);
    }
});
AlertDialog alert = builder.create();

...

// Now elsewhere in your Activity class, you would have this function
private void myFunction(int result){
    // Now the data has been "returned" (as pointed out, that's not
    // the right terminology)
}

Solution 2 - Android

For my MIDI app I needed yes/no/cancel confirmation dialogs, so I first made a general StandardDialog class:

public class StandardDialog {

    import android.app.Activity;
    import android.app.AlertDialog;
    import android.content.DialogInterface;
    import android.os.Handler;

    public class StandardDialog {
    public static final int dlgResultOk			= 0;
	public static final int dlgResultYes 		= 1;
	public static final int dlgResultNo 		= 2;
	public static final int dlgResultCancel		= 3;

	public static final int dlgTypeOk 			= 10;
	public static final int dlgTypeYesNo		= 11;
	public static final int dlgTypeYesNoCancel	= 12;

	private Handler mResponseHandler;
	private AlertDialog.Builder mDialogBuilder;
	private int mDialogId;

	public StandardDialog(Activity parent, 
                          Handler reponseHandler, 
                          String title, 
                          String message, 
                          int dialogType, 
                          int dialogId) {

		mResponseHandler = reponseHandler;
		mDialogId = dialogId;
		mDialogBuilder = new AlertDialog.Builder(parent);
		mDialogBuilder.setCancelable(false);
		mDialogBuilder.setTitle(title);
		mDialogBuilder.setIcon(android.R.drawable.ic_dialog_alert);
		mDialogBuilder.setMessage(message);
		switch (dialogType) {
		case dlgTypeOk:
			mDialogBuilder.setNeutralButton("Ok", new DialogInterface.OnClickListener() {
				public void onClick(DialogInterface dialog, int which) {
					mResponseHandler.sendEmptyMessage(mDialogId + dlgResultOk);
				}
			});			
			break;

		case dlgTypeYesNo:
		case dlgTypeYesNoCancel:
			mDialogBuilder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
				public void onClick(DialogInterface dialog, int which) {
					mResponseHandler.sendEmptyMessage(mDialogId + dlgResultYes);
				}
			});			
			mDialogBuilder.setNegativeButton("No", new DialogInterface.OnClickListener() {
				public void onClick(DialogInterface dialog, int which) {
					mResponseHandler.sendEmptyMessage(mDialogId + dlgResultNo);
				}
			});			
			if (dialogType == dlgTypeYesNoCancel) {
				mDialogBuilder.setNeutralButton("Cancel", new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int which) {
						mResponseHandler.sendEmptyMessage(mDialogId + dlgResultCancel);
					}
				});			
			}
			break;
		}
		mDialogBuilder.show();
	}
}

Next, in my main activity I already had a message handler for the UI updates from other threads, so I just added code for processing messages from the dialogs. By using a different dialogId parameter when I instantiate the StandardDialog for various program functions, I can execute the proper code to handle the yes/no/cancel responses to different questions. This idea can be extended for complex custom dialogs by sending a Bundle of data though this is much slower than a simple integer message.

private Handler uiMsgHandler = new Handler() {
		
    @Override
	public void handleMessage(Message msg) {
	    if (msg != null) {
    
            // {Code to check for other UI messages here}
    
    		// Check for dialog box responses
			if (msg.what == (clearDlgId + StandardDialog.dlgResultYes)) {
				doClearDlgYesClicked();
			}
			else if (msg.what == (recordDlgId + StandardDialog.dlgResultYes)) {
				doRecordDlgYesClicked();
			}
			else if (msg.what == (recordDlgId + StandardDialog.dlgResultNo)) {
				doRecordDlgNoClicked();
			}
		}
	}
};

Then all I need to do is define the do{Whatever}() methods in the activity. To bring up a dialog, as an example I have a method responding to a "clear recorded MIDI events" button and confirm it as follows:

public void onClearBtnClicked(View view) {
	new StandardDialog(this, uiMsgHandler, 
        getResources().getString(R.string.dlgTitleClear),
		getResources().getString(R.string.dlgMsgClear), 
        StandardDialog.dlgTypeYesNo, clearDlgId);
}

clearDlgId is defined as a unique integer elsewhere. This method makes a Yes/No dialog pop up in front of the activity, which loses focus until the dialog closes, at which time the activity gets a message with the dialog result. Then the message handler calls the doClearDlgYesClicked() method if the "Yes" button was clicked. (I didn't need a message for the "No" button since no action was needed in that case).

Anyway, this method works for me, and makes it easy to pass results back from a dialog.

Solution 3 - Android

I'm using following way:

  1. All my activities has one and the same parent Activity (let's say ControlActivity). ControlActivity has private volatile Bundle controlBundle; with appropriate getter/setter

  2. When I start dialog, I used to call dialog thru my own method:

    public void showMyDialog(int id, Bundle bundle)
    {
        this.controlBundle=bundle;
        this.showDialog(id, bundle);
    }
    

So each time I know parameters sent to dialog

  1. When dialog is about to complete, I'm forming in dialog another Bundle with necessary values and then put them thru my Activity bundle setter:


((ControlActivity )this.getOwnerActivity).setControlBundle(bundle);

So in the end when dialog finishes I know value "returned" from dialog. I know that it's not like int retCode=this.showMyDialog(); it's a bit more complex, but it's workable.

Solution 4 - Android

I've pondered over this myself for a while and eventually the most convenient way I found of doing this is breaking up my activity into various methods that represent each unit of control flow. For example, if the activities within my activity are say : load variables from intent, check for some data, process and proceed if available, if not make a background call, wait for user interaction, start another activity.

I generally pickup the parts that are common, the first two and the last in this case. I'll wrap the first ones in onCreate() and make a separate one for the last... say startAnotherActivity(Data). You can arrange the middle parts so they'll consist of a checkData(Data) (possibly merged into the onCreate()) which calls either processAvailableData(Data) or performBackgroundTask(Data). The background task will perform an operation in the background and return control to onBackgroundTaskCompleted(OtherData).

Now both processAvailableData(Data) and onBackgroundTaskCompleted(OtherData) call the getUserResponse() method which in turn may either call startAnotherActivity(Data) or merge its functions with itself.

I feel this approach gives a number of benefits.

  1. It helps with the data return issue your question points to by "moving forwards" instead of returning data.
  2. It allows easier addition of new functionality. For instance, if we wanted to give the user more options, we could just call the appropriate method from getUserResponse() which could effect the data that is eventually passed to the next activity.
  3. It helps avoid needless flow problems (check out the questions relating to finish() and return on SO) when our intuitive assumption is a certain flow and it turns out to be another.
  4. Helps manage variables better so you don't end up having a lot of class level fields to avoid the variable access problems in anonymous inner classes (onClick(), doInBackground(), etc.).

I'm pretty sure having more methods adds some overhead but its probably offset by the flow, reuse and simplicity advantages you get (would love to hear a compilation expert's views on this).

Solution 5 - Android

I will explain one solution with the example of two fragments. Imagine there is a SimpleFragment which has just one text field to render a date. Then there is a DatePickerFragment which allows to choose a particular date. What I want is that the DatePickerFragment passes the date value back to the calling SimpleFragment whenever the user confirms her selection.

SimpleFragment

So, first of all we start the DatePickerFragment from within the SimpleFragment:

private DateTime mFavoriteDate; // Joda-Time date

private void launchDatePicker() {
    DatePickerFragment datePickerFragment = new DatePickerFragment();
    Bundle extras = new Bundle();
    // Pass an initial or the last value for the date picker
    long dateInMilliSeconds = mFavoriteDate.getMillis();
    extras.putLong(BundleKeys.LAST_KNOWN_DATE, dateInMilliSeconds);
    datePickerFragment.setArguments(extras);
    datePickerFragment.setTargetFragment(this, SIMPLE_FRAGMENT_REQUEST_CODE);
    datePickerFragment.show(getActivity().getSupportFragmentManager(),
            DatePickerFragment.FRAGMENT_TAG);
}

DatePickerFragment

In the dialog fragment we prepare to pass back the selected date when the user hits the positive button:

public static final String DATE_PICKED_INTENT_KEY = "DATE_PICKED_INTENT_KEY";
public static final int DATE_PICKED_RESULT_CODE = 123;

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    // ...
    Long dateInMilliSeconds = getArguments().getLong(BundleKeys.LAST_KNOWN_DATE);
    DateTime date = new DateTime(dateInMilliSeconds);
    initializePickerUiControl(date);

    AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(activity);
    dialogBuilder
        .setPositiveButton(R.string.date_picker_positive, (dialog, which) -> {
            // Pass date to caller
            passBackDate();
        })
        .setNegativeButton(R.string.date_picker_negative, (dialog, which) -> {
            // Nothing to do here
        });
    return dialogBuilder.create();
}

private void passBackDate() {
    DateTime dateTime = getDateTimeFromPickerControl();
    Intent intent = new Intent();
    intent.putExtra(DATE_PICKED_INTENT_KEY, dateTime.getMillis());
    getTargetFragment().onActivityResult(
            getTargetRequestCode(), DATE_PICKED_RESULT_CODE, intent);
}

SimpleFragment

Back in the requesting fragment we consume what has been passed back by the dialog:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == SIMPLE_FRAGMENT_REQUEST_CODE &&
            resultCode == DatePickerFragment.DATE_PICKED_RESULT_CODE) {
        long datePickedInMilliseconds = data.getLongExtra(
                DatePickerFragment.DATE_PICKED_INTENT_KEY, 0);
        mFavoriteDate = new DateTime(datePickedInMilliseconds);
        updateFavoriteDateTextView();
    }
    else {
        super.onActivityResult(requestCode, resultCode, data);
    }
}

References to mattpic who gave an excellent answer before.

Solution 6 - Android

After quite a bit of research I settled on a callback interface. My code is as follows:

MyFragment.java

public class MyFragment extends Fragment {

...

private void displayFilter() {

    FragmentManager fragmentManager = getFragmentManager();

    FilterDialogFragment filterDialogFragment = new FilterDialogFragment();
    Bundle bundle = new Bundle();
    bundle.putSerializable("listener", new FilterDialogFragment.OnFilterClickListener() {
        @Override
        public void onFilterClickListener() {
            System.out.println("LISTENER CLICKED");
            
        }
    });
    filterDialogFragment.setArguments(bundle);
    filterDialogFragment.show(fragmentManager, DIALOG_FILTER);

}

MyDialog.java

public class MyDialog extends DialogFragment {

private ImageButton mBtnTest;
private OnFilterClickListener mOnFilterClickListener;

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
    // Get the layout inflater
    LayoutInflater inflater = getActivity().getLayoutInflater();
    View filterLayout = inflater.inflate(R.layout.filter_dialog, null);
    // Inflate and set the layout for the dialog
    // Pass null as the parent view because its going in the dialog layout
    builder.setView(filterLayout)
            .setTitle("Filter");

    Dialog dialog = builder.create();

    mOnFilterClickListener = (OnFilterClickListener) getArguments().getSerializable("listener");

    mBtnTest = (ImageButton)filterLayout.findViewById(R.id.fandb);
    mBtnTest.setOnClickListener(new View.OnClickListener() {
        public void onClick(View v) {
            mOnFilterClickListener.onFilterClickListener();
            dismiss();
        }
    });

    return dialog;
}

public interface OnFilterClickListener extends Serializable {
    void onFilterClickListener();
}

}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPeter NelsonView Question on Stackoverflow
Solution 1 - AndroidComputerishView Answer on Stackoverflow
Solution 2 - AndroidBrian BaileyView Answer on Stackoverflow
Solution 3 - AndroidBarmaleyView Answer on Stackoverflow
Solution 4 - AndroidSaad FarooqView Answer on Stackoverflow
Solution 5 - AndroidJJDView Answer on Stackoverflow
Solution 6 - AndroidBraden HoltView Answer on Stackoverflow