Google recommends that we use DialogFragment
instead of a simple Dialog
by using Fragments API
, but it is absurd to use an isolated DialogFragment
for a simple Yes-No confirmation message box. What is the best practice in this case?
问题:
回答1:
Yes, use DialogFragment
and in onCreateDialog
you can simply use an AlertDialog builder anyway to create a simple AlertDialog
with Yes/No confirmation buttons. Not very much code at all.
With regards handling events in your fragment there would be various ways of doing it but I simply define a message Handler
in my Fragment
, pass it into the DialogFragment
via its constructor and then pass messages back to my fragment's handler as approprirate on the various click events. Again various ways of doing that but the following works for me.
In the dialog hold a message and instantiate it in the constructor:
private Message okMessage;
...
okMessage = handler.obtainMessage(MY_MSG_WHAT, MY_MSG_OK);
Implement the onClickListener
in your dialog and then call the handler as appropriate:
public void onClick(.....
if (which == DialogInterface.BUTTON_POSITIVE) {
final Message toSend = Message.obtain(okMessage);
toSend.sendToTarget();
}
}
Edit
And as Message
is parcelable you can save it out in onSaveInstanceState
and restore it
outState.putParcelable("okMessage", okMessage);
Then in onCreate
if (savedInstanceState != null) {
okMessage = savedInstanceState.getParcelable("okMessage");
}
回答2:
You can create generic DialogFragment subclasses like YesNoDialog and OkDialog, and pass in title and message if you use dialogs a lot in your app.
public class YesNoDialog extends DialogFragment
{
public static final String ARG_TITLE = "YesNoDialog.Title";
public static final String ARG_MESSAGE = "YesNoDialog.Message";
public YesNoDialog()
{
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
Bundle args = getArguments();
String title = args.getString(ARG_TITLE);
String message = args.getString(ARG_MESSAGE);
return new AlertDialog.Builder(getActivity())
.setTitle(title)
.setMessage(message)
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
getTargetFragment().onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, null);
}
})
.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
getTargetFragment().onActivityResult(getTargetRequestCode(), Activity.RESULT_CANCELED, null);
}
})
.create();
}
}
Then call it using the following:
DialogFragment dialog = new YesNoDialog();
Bundle args = new Bundle();
args.putString(YesNoDialog.ARG_TITLE, title);
args.putString(YesNoDialog.ARG_MESSAGE, message);
dialog.setArguments(args);
dialog.setTargetFragment(this, YES_NO_CALL);
dialog.show(getFragmentManager(), "tag");
And handle the result in onActivityResult
.
回答3:
Use DialogFragment over AlertDialog:
Since the introduction of API level 13:
the showDialog method from Activity is deprecated. Invoking a dialog elsewhere in code is not advisable since you will have to manage the the dialog yourself (e.g. orientation change).
Difference DialogFragment - AlertDialog
Are they so much different? From Android reference regarding DialogFragment:
A DialogFragment is a fragment that displays a dialog window, floating on top of its activity's window. This fragment contains a Dialog object, which it displays as appropriate based on the fragment's state. Control of the dialog (deciding when to show, hide, dismiss it) should be done through the API here, not with direct calls on the dialog.
Other notes
- Fragments are a natural evolution in the Android framework due to the diversity of devices with different screen sizes.
- DialogFragments and Fragments are made available in the support library which makes the class usable in all current used versions of Android.
回答4:
I would recommend using DialogFragment
.
Sure, creating a "Yes/No" dialog with it is pretty complex considering that it should be rather simple task, but creating a similar dialog box with Dialog
is surprisingly complicated as well.
(Activity lifecycle makes it complicated - you must let Activity
manage the lifecycle of the dialog box - and there is no way to pass custom parameters e.g. the custom message to Activity.showDialog
if using API levels under 8)
The nice thing is that you can usually build your own abstraction on top of DialogFragment
pretty easily.
回答5:
Generic AlertDialogFragment with Builder Pattern
In my project, I already used AlertDialog.Builder
already a lot before I found out that it's problematic. However, I did not want to change that much code anywhere in my app. Additionally, I actually am a fan of passing OnClickListeners
as anonymous classes where they are needed (that is, when using setPositiveButton()
, setNegativeButton()
etc.) instead of having to implement thousands of callback methods to communicate between a dialog fragment and the holder fragment, which can, in my opinion, lead to very confusing and complex code. Especially, if you have multiple different dialogs in one fragment and then need to distinguish in the callback implementations between which dialog currently being shown.
Therefore, I combined different approaches to create a generic AlertDialogFragment
helper class which can be used exactly like AlertDialog
:
SOLUTION
(PLEASE NOTE that I am using Java 8 lambda expressions in my code, so you might have to change parts of the code if you are not using lambda expressions yet.)
/**
* Helper class for dialog fragments to show a {@link AlertDialog}. It can be used almost exactly
* like a {@link AlertDialog.Builder}
* <p />
* Creation Date: 22.03.16
*
* @author felix, http://flx-apps.com/
*/
public class AlertDialogFragment extends DialogFragment {
protected FragmentActivity activity;
protected Bundle args;
protected String tag = AlertDialogFragment.class.getSimpleName();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activity = getActivity();
args = getArguments();
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dialog = setDialogDefaults(new AlertDialog.Builder(getActivity())).create();
if (args.containsKey("gravity")) {
dialog.getWindow().getAttributes().gravity = args.getInt("gravity");
}
dialog.setOnShowListener(d -> {
if (dialog != null && dialog.findViewById((android.R.id.message)) != null) {
((TextView) dialog.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance());
}
});
return dialog;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return super.onCreateView(inflater, container, savedInstanceState);
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
if (args.containsKey("onDismissListener")) {
Parcelable onDismissListener = args.getParcelable("onDismissListener");
if (onDismissListener != null && onDismissListener instanceof ParcelableOnDismissListener) {
((ParcelableOnDismissListener) onDismissListener).onDismiss(this);
}
}
}
/**
* Sets default dialog properties by arguments which were set using {@link #builder(FragmentActivity)}
*/
protected AlertDialog.Builder setDialogDefaults(AlertDialog.Builder builder) {
args = getArguments();
activity = getActivity();
if (args.containsKey("title")) {
builder.setTitle(args.getCharSequence("title"));
}
if (args.containsKey("message")) {
CharSequence message = args.getCharSequence("message");
builder.setMessage(message);
}
if (args.containsKey("viewId")) {
builder.setView(getActivity().getLayoutInflater().inflate(args.getInt("viewId"), null));
}
if (args.containsKey("positiveButtonText")) {
builder.setPositiveButton(args.getCharSequence("positiveButtonText"), (dialog, which) -> {
onButtonClicked("positiveButtonListener", which);
});
}
if (args.containsKey("negativeButtonText")) {
builder.setNegativeButton(args.getCharSequence("negativeButtonText"), (dialog, which) -> {
onButtonClicked("negativeButtonListener", which);
});
}
if (args.containsKey("neutralButtonText")) {
builder.setNeutralButton(args.getCharSequence("neutralButtonText"), (dialog, which) -> {
onButtonClicked("neutralButtonListener", which);
});
}
if (args.containsKey("items")) {
builder.setItems(args.getStringArray("items"), (dialog, which) -> {
onButtonClicked("itemClickListener", which);
});
}
// @formatter:off
// FIXME this a pretty hacky workaround: we don't want to show the dialog if onClickListener of one of the dialog's button click listener were lost
// the problem is, that there is no (known) solution for parceling a OnClickListener in the long term (only for state changes like orientation change,
// but not if the Activity was completely lost)
if (
(args.getParcelable("positiveButtonListener") != null && !(args.getParcelable("positiveButtonListener") instanceof ParcelableOnClickListener)) ||
(args.getParcelable("negativeButtonListener") != null && !(args.getParcelable("negativeButtonListener") instanceof ParcelableOnClickListener)) ||
(args.getParcelable("neutralButtonListener") != null && !(args.getParcelable("neutralButtonListener") instanceof ParcelableOnClickListener)) ||
(args.getParcelable("itemClickListener") != null && !(args.getParcelable("itemClickListener") instanceof ParcelableOnClickListener))
) {
new DebugMessage("Forgot onClickListener. Needs to be dismissed.")
.logLevel(DebugMessage.LogLevel.VERBOSE)
.show();
try {
dismissAllowingStateLoss();
} catch (NullPointerException | IllegalStateException ignored) {}
}
// @formatter:on
return builder;
}
public interface OnDismissListener {
void onDismiss(AlertDialogFragment dialogFragment);
}
public interface OnClickListener {
void onClick(AlertDialogFragment dialogFragment, int which);
}
protected void onButtonClicked(String buttonKey, int which) {
ParcelableOnClickListener parcelableOnClickListener = getArguments().getParcelable(buttonKey);
if (parcelableOnClickListener != null) {
parcelableOnClickListener.onClick(this, which);
}
}
// region Convenience Builder Pattern class almost similar to AlertDialog.Builder
// =============================================================================================
public AlertDialogFragment builder(FragmentActivity activity) {
this.activity = activity;
this.args = new Bundle();
return this;
}
public AlertDialogFragment addArguments(Bundle bundle) {
args.putAll(bundle);
return this;
}
public AlertDialogFragment setTitle(int titleStringId) {
return setTitle(activity.getString(titleStringId));
}
public AlertDialogFragment setTitle(CharSequence title) {
args.putCharSequence("title", title);
return this;
}
public AlertDialogFragment setMessage(int messageStringId) {
return setMessage(activity.getString(messageStringId));
}
public AlertDialogFragment setMessage(CharSequence message) {
args.putCharSequence("message", message);
return this;
}
public AlertDialogFragment setPositiveButton(int textStringId, OnClickListener onClickListener) {
return setPositiveButton(activity.getString(textStringId), onClickListener);
}
public AlertDialogFragment setPositiveButton(CharSequence text, AlertDialogFragment.OnClickListener onClickListener) {
args.putCharSequence("positiveButtonText", text);
args.putParcelable("positiveButtonListener", createParcelableOnClickListener(onClickListener));
return this;
}
public AlertDialogFragment setNegativeButton(int textStringId, AlertDialogFragment.OnClickListener onClickListener) {
return setNegativeButton(activity.getString(textStringId), onClickListener);
}
public AlertDialogFragment setNegativeButton(CharSequence text, AlertDialogFragment.OnClickListener onClickListener) {
args.putCharSequence("negativeButtonText", text);
args.putParcelable("negativeButtonListener", createParcelableOnClickListener(onClickListener));
return this;
}
public AlertDialogFragment setNeutralButton(int textStringId, AlertDialogFragment.OnClickListener onClickListener) {
return setNeutralButton(activity.getString(textStringId), onClickListener);
}
public AlertDialogFragment setNeutralButton(CharSequence text, AlertDialogFragment.OnClickListener onClickListener) {
args.putCharSequence("neutralButtonText", text);
args.putParcelable("neutralButtonListener", createParcelableOnClickListener(onClickListener));
return this;
}
public AlertDialogFragment setOnDismissListener(OnDismissListener onDismissListener) {
if (onDismissListener == null) {
return this;
}
Parcelable p = new ParcelableOnDismissListener() {
@Override
public void onDismiss(AlertDialogFragment dialogFragment) {
onDismissListener.onDismiss(dialogFragment);
}
};
args.putParcelable("onDismissListener", p);
return this;
}
public AlertDialogFragment setItems(String[] items, AlertDialogFragment.OnClickListener onClickListener) {
args.putStringArray("items", items);
args.putParcelable("itemClickListener", createParcelableOnClickListener(onClickListener));
return this;
}
public AlertDialogFragment setView(int viewId) {
args.putInt("viewId", viewId);
return this;
}
public AlertDialogFragment setGravity(int gravity) {
args.putInt("gravity", gravity);
return this;
}
public AlertDialogFragment setTag(String tag) {
this.tag = tag;
return this;
}
public AlertDialogFragment create() {
setArguments(args);
return AlertDialogFragment.this;
}
public AlertDialogFragment show() {
create();
try {
super.show(activity.getSupportFragmentManager(), tag);
}
catch (IllegalStateException e1) {
/**
* this whole part is used in order to attempt to show the dialog if an
* {@link IllegalStateException} was thrown (it's kinda comparable to
* {@link FragmentTransaction#commitAllowingStateLoss()}
* So you can remove all those dirty hacks if you are sure that you are always
* properly showing dialogs in the right moments
*/
new DebugMessage("got IllegalStateException attempting to show dialog. trying to hack around.")
.logLevel(DebugMessage.LogLevel.WARN)
.exception(e1)
.show();
try {
Field mShownByMe = DialogFragment.class.getDeclaredField("mShownByMe");
mShownByMe.setAccessible(true);
mShownByMe.set(this, true);
Field mDismissed = DialogFragment.class.getDeclaredField("mDismissed");
mDismissed.setAccessible(true);
mDismissed.set(this, false);
}
catch (Exception e2) {
new DebugMessage("error while showing dialog")
.exception(e2)
.logLevel(DebugMessage.LogLevel.ERROR)
.show();
}
FragmentTransaction transaction = activity.getSupportFragmentManager().beginTransaction();
transaction.add(this, tag);
transaction.commitAllowingStateLoss(); // FIXME hacky and unpredictable workaround
}
return AlertDialogFragment.this;
}
@Override
public int show(FragmentTransaction transaction, String tag) {
throw new NoSuchMethodError("Please use AlertDialogFragment.show()!");
}
@Override
public void show(FragmentManager manager, String tag) {
throw new NoSuchMethodError("Please use AlertDialogFragment.show()!");
}
protected ParcelableOnClickListener createParcelableOnClickListener(AlertDialogFragment.OnClickListener onClickListener) {
if (onClickListener == null) {
return null;
}
return new ParcelableOnClickListener() {
@Override
public void onClick(AlertDialogFragment dialogFragment, int which) {
onClickListener.onClick(dialogFragment, which);
}
};
}
/**
* Parcelable OnClickListener (can be remembered on screen rotation)
*/
public abstract static class ParcelableOnClickListener extends ResultReceiver implements AlertDialogFragment.OnClickListener {
public static final Creator<ResultReceiver> CREATOR = ResultReceiver.CREATOR;
ParcelableOnClickListener() {
super(null);
}
@Override
public abstract void onClick(AlertDialogFragment dialogFragment, int which);
}
/**
* Parcelable OnDismissListener (can be remembered on screen rotation)
*/
public abstract static class ParcelableOnDismissListener extends ResultReceiver implements AlertDialogFragment.OnDismissListener {
public static final Creator<ResultReceiver> CREATOR = ResultReceiver.CREATOR;
ParcelableOnDismissListener() {
super(null);
}
@Override
public abstract void onDismiss(AlertDialogFragment dialogFragment);
}
// =============================================================================================
// endregion
}
USAGE
// showing a normal alert dialog with state loss on configuration changes (like device rotation)
new AlertDialog.Builder(getActivity())
.setTitle("Are you sure? (1)")
.setMessage("Do you really want to do this?")
.setPositiveButton("Yes", (dialog, which) -> Toast.makeText(getContext(), "Yes clicked", Toast.LENGTH_SHORT).show())
.setNegativeButton("Cancel", null)
.show();
// showing a dialog fragment using the helper class with no state loss on configuration changes
new AlertDialogFragment.builder(getActivity())
.setTitle("Are you sure? (2)")
.setMessage("Do you really want to do this?")
.setPositiveButton("Yes", (dialog, which) -> Toast.makeText(getContext(), "Yes clicked", Toast.LENGTH_SHORT).show())
.setNegativeButton("Cancel", null)
.show();
I am posting this here not only to share my solution, but also because I wanted to ask you people for your opinion: Is this approach legit or problematic to some extent?
回答6:
May I suggest a little simplification of @ashishduh's answer:
public class AlertDialogFragment extends DialogFragment {
public static final String ARG_TITLE = "AlertDialog.Title";
public static final String ARG_MESSAGE = "AlertDialog.Message";
public static void showAlert(String title, String message, Fragment targetFragment) {
DialogFragment dialog = new AlertDialogFragment();
Bundle args = new Bundle();
args.putString(ARG_TITLE, title);
args.putString(ARG_MESSAGE, message);
dialog.setArguments(args);
dialog.setTargetFragment(targetFragment, 0);
dialog.show(targetFragment.getFragmentManager(), "tag");
}
public AlertDialogFragment() {}
@NonNull
@Override
public AlertDialog onCreateDialog(Bundle savedInstanceState)
{
Bundle args = getArguments();
String title = args.getString(ARG_TITLE, "");
String message = args.getString(ARG_MESSAGE, "");
return new AlertDialog.Builder(getActivity())
.setTitle(title)
.setMessage(message)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
getTargetFragment().onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, null);
}
})
.create();
}
It removes the need for the user (of the class) to be familiar with the internals of the component and makes usage really simple:
AlertDialogFragment.showAlert(title, message, this);
P.S. In my case I needed a simple alert dialog so that's what I created. You can apply the approach to a Yes/No or any other type you need.