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,
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.
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);
}
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);
}
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.
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)
}
I'm using following way:
private volatile Bundle controlBundle;
with appropriate getter/setterWhen 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
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.
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();
}
}
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.
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.
getUserResponse()
which could effect the data that is eventually passed to the next activity.finish()
and return
on SO) when our intuitive assumption is a certain flow and it turns out to be another.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).