Android DialogFragment vs Dialog

Posted

技术标签:

【中文标题】Android DialogFragment vs Dialog【英文标题】: 【发布时间】:2011-12-20 02:45:07 【问题描述】:

Google 建议我们通过使用Fragments API 来使用DialogFragment 而不是简单的Dialog,但是对于简单的Yes-No 确认消息框使用孤立的DialogFragment 是荒谬的。在这种情况下,最佳做法是什么?

【问题讨论】:

简而言之,除其他外,简单的DialogAlertDialog.Builder::create()::show() 将创建一个对话框,当您旋转屏幕时会消失。 【参考方案1】:

是的,使用DialogFragment 并且在onCreateDialog 中,无论如何您都可以简单地使用AlertDialog 构建器来创建一个简单的AlertDialog,并带有是/否确认按钮。根本没有太多代码。

关于处理片段中的事件,有多种方法可以做到,但我只是在我的Fragment 中定义一条消息Handler,通过它的构造函数将它传递给DialogFragment,然后将消息传递回我的片段的处理程序适用于各种点击事件。同样有多种方法可以做到这一点,但以下方法对我有用。

在对话框中保存一条消息并在构造函数中实例化它:

private Message okMessage;
...
okMessage = handler.obtainMessage(MY_MSG_WHAT, MY_MSG_OK);

在您的对话框中实现onClickListener,然后根据需要调用处理程序:

public void onClick(.....
    if (which == DialogInterface.BUTTON_POSITIVE) 
        final Message toSend = Message.obtain(okMessage);
        toSend.sendToTarget();
    
 

编辑

由于Message 是可包裹的,您可以将其保存在onSaveInstanceState 中并恢复它

outState.putParcelable("okMessage", okMessage);

然后在onCreate

if (savedInstanceState != null) 
    okMessage = savedInstanceState.getParcelable("okMessage");

【讨论】:

问题不在于 okMes​​sage - 问题在于 okMes​​sage 的 target,如果您从 Bundle 加载它,它将为空。如果 Message 的目标为 null,并且您使用 sendToTarget,您将收到 NullPointerException - 不是因为 Message 为 null,而是因为它的目标是。 使用 DialogFragment 代替 Dialog 有什么好处? 使用 DialogFragment 的好处是对话框的所有生命周期都将为您处理。您将永远不会再次收到错误“对话框已泄漏...”。转到 DialogFragment 并忘记 Dialogs。 我认为应该使用 setArguments() 和 getArguments() 而不是通过构造函数传入 okMes​​sage。 好吧,我很容易成为用户生成器,我用这个 android:configChanges="locale|keyboardHidden|orientation|screenSize" 处理活动管理,我没有看到应用程序有任何问题......【参考方案2】:

如果您在应用程序中大量使用对话框,您可以创建通用 DialogFragment 子类,例如 YesNoDialog 和 OkDialog,并传入标题和消息。

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();
    

然后使用以下方法调用它:

    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");

并在onActivityResult处理结果。

【讨论】:

是的,DialogFragment 为您处理所有生命周期事件。 我认为它不是因为在旋转后旧对话框仍然存在并且它保持分配给旧的不存在的片段 (dialog.setTargetFragment(this, YES_NO_CALL);) 所以在旋转后 getTargetFragment().onActivityResult 没有不工作 YES_NO_CALLgetFragmentManager()onActivityResult 是什么? YES_NO_CALL 是一个自定义 int,它是请求代码。 getFragmentManager() 获取活动的片段管理器,onActivityResult() 是片段生命周期回调方法。 将 getFragmentManager() 替换为 getSupportFragmentManager();【参考方案3】:

在 AlertDialog 上使用 DialogFragment:


自从引入 API 级别 13

Activity 中的showDialog 方法已弃用。 不建议在代码的其他地方调用对话框,因为您必须自己管理对话框(例如方向更改)。

Difference DialogFragment - AlertDialog

它们有那么大的不同吗?来自关于DialogFragment的Android参考:

DialogFragment 是一个显示对话窗口的片段,浮动在其顶部 活动的窗口。这个片段包含一个 Dialog 对象,它 根据片段的状态适当显示。的控制 应该完成对话框(决定何时显示、隐藏、关闭它) 通过 API here,而不是直接调用对话框。

其他说明

由于具有不同屏幕尺寸的设备的多样性,片段是 Android 框架中的自然演变。 DialogFragments 和 Fragments 在支持库中可用,这使得该类可用于所有当前使用的 Android 版本。

【讨论】:

【参考方案4】:

我建议使用DialogFragment

当然,使用它创建一个“是/否”对话框非常复杂,因为它应该是相当简单的任务,但是使用Dialog 创建一个类似的对话框也非常复杂。

(活动生命周期变得复杂 - 你必须让Activity 管理对话框的生命周期 - 如果使用低于 8 的 API 级别,则无法将自定义参数(例如自定义消息)传递给 Activity.showDialog

好消息是您通常可以很容易地在 DialogFragment 之上构建自己的抽象。

【讨论】:

您将如何处理警报对话框回调(是,否)? 最简单的方法是在托管 Activity 中实现一个采用String 参数的方法。例如,当用户单击“是”时,对话框会调用带有参数“同意”的 Activity 方法。这些参数是在显示对话框时指定的,例如 AskDialog.ask("Do you agree to these terms?", "agree", "disagree"); 但我需要在片段内回调,而不是活动。我可以使用 setTargetFragment 并将其转换为接口。但这是地狱。 您还可以通过为目标设置标签并使用FragmentManagerfindFragmentByTag 来获取目标片段。但是,是的,它需要相当多的代码。 @AlexeyZakharov 我知道这大约晚了 5 年,但你可以通过 Fragment this 并拥有你的 Activity extends 你的 Interface。不过要小心线程,如果您的并发性未得到检查,您可能会在不一定需要接口调用时弹出接口调用。不知道这对内存和循环依赖意大利面有什么影响,还有其他人愿意加入吗?另一个选项是Message/Handler,但您仍然可能遇到并发问题。【参考方案5】:

具有生成器模式的通用 AlertDialogFragment

在我的项目中,我已经经常使用AlertDialog.Builder,然后才发现它有问题。但是,我不想在我的应用程序的任何地方更改那么多代码。此外,我实际上喜欢将OnClickListeners 作为匿名类传递到需要它们的地方(即,在使用setPositiveButton()setNegativeButton() 等时),而不必实现数千个回调方法来在对话框片段之间进行通信和持有者片段,在我看来,这可能会导致非常混乱和复杂的代码。特别是,如果您在一个片段中有多个不同的对话框,然后需要在回调实现中区分当前显示的是哪个对话框。

因此,我结合了不同的方法来创建一个通用的 AlertDialogFragment 帮助类,它可以像 完全一样使用AlertDialog


解决方案

请注意我在我的代码中使用了 Java 8 lambda 表达式,因此如果您还没有使用 lambda expressions,您可能需要更改部分代码。)

/**
 * 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

用法

// 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();

我在这里发布这个不仅是为了分享我的解决方案,也是因为我想向你们征求意见:这种方法在某种程度上是合法的还是有问题的?

【讨论】:

这是一个非常有趣的想法,但我认为 API 设计行不通。如果将 OnClickListener 传递给 setPositiveButton(),则当设备旋转并从 Bundle 参数重新创建片段时,将无法从 Parcelable 正确重新创建 OnClickListener。基本问题是您无法在轮换期间重新创建侦听器,但 API 接口(采用接口)需要它。我希望不是这样(因为我喜欢这个主意)。 好主意,但正如@Xargs 所说,它不起作用。传入的侦听器在轮换时未正确重新创建。 我的结果是它实际上适用于旋转和恢复到应用程序(例如,在转到主屏幕之后),但当 Activity 在完全销毁后恢复时(然后OnClickListeners 确实丢失了)。 (在 Android 4.4.4 和 Android 5.1.1 上测试) 我还没有测试过这个确切的实现,但是根据我的测试,传递给片段包的可打包侦听器在重新创建时被正确调用。我不知道为什么,但它似乎有效。 @flxapps,如果是自定义视图,您如何获取子视图并更改其属性或应用侦听器?在您的课程中,您没有返回任何对话框实例,如果有人试图获取子视图,这可能会导致异常【参考方案6】:

我可以建议对@ashishduh 的回答进行一些简化:

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();

它消除了(类的)用户熟悉组件内部的需要,并使使用变得非常简单:

AlertDialogFragment.showAlert(title, message, this);

附:就我而言,我需要一个简单的警报对话框,这就是我创建的。您可以将该方法应用于是/否或您需要的任何其他类型。

【讨论】:

【参考方案7】:

DialogFragment 基本上是一个可以用作对话框的 Fragment。

由于以下原因,使用 DialogFragment 而不是 Dialog:

DialogFragment 在配置更改和保存和恢复流程后自动重新创建 DialogFragment 继承了 Fragment 的完整生命周期 不再有 IllegalStateExceptions 和泄漏的窗口崩溃。当活动被警报对话框破坏时,这很常见 还在那里。

More detail

【讨论】:

【参考方案8】:

使用 Dialog 进行简单的是或否对话框。

当您需要更复杂的视图时,您需要掌握生命周期,例如 oncreate、请求权限、任何生命周期覆盖,我会使用对话框片段。因此,您可以将权限和对话框需要操作的任何其他代码分开,而无需与调用活动进行通信。

【讨论】:

【参考方案9】:

DialogFragment 具有对话框和片段的功能。基本上所有生命周期事件都可以通过 DialogFragment 自动管理得很好,例如屏幕配置的更改等。

【讨论】:

【参考方案10】:

对话框:对话框是提示用户做出决定或输入其他信息的小窗口。

DialogFragment: DialogFragment 是一个特殊的片段子类,专为创建和托管对话框而设计。它允许 FragmentManager 管理对话框的状态,并在发生配置更改时自动恢复对话框。

【讨论】:

以上是关于Android DialogFragment vs Dialog的主要内容,如果未能解决你的问题,请参考以下文章

Android 撸起袖子,自己封装 DialogFragment

Android 撸起袖子,自己封装 DialogFragment

Android中的全屏DialogFragment

主题不适用于 Android 上的 DialogFragment

Android P 中弃用的 DialogFragment 类

Android自定义DialogFragment问题[重复]