【android】Activityから呼ぶ汎用YesNoダイアログ


YesNoダイアログを作りました。

VBAとか、C#だとまぁ、こう書けるわけですよ。

~~~vb
    If MsgBox("削除していい?", vbOKCancel, "確認") = vbOK Then
        'DeleteProcess
    End If
~~~
![dialog01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1443439/382bc702-8b12-e798-fed4-0fdeefd2463a.png)

で、Androidでこれに似たようなものというと、FragmentDialogというのがあって
![dialog02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1443439/39fc774e-134d-e3e7-83a6-087a4e7daca5.png)

こんな感じの。

ただ、やはりというか、そもそもAndroidではモーダル表示という概念がなさそうなので、そのままのイメージでは実装できないんですよね。サンプルのベースはこれ。

https://developer.android.com/guide/topics/ui/dialogs?hl=ja

「ミサイルを撃ちますか？」「Cancel」「Fire!」というネタは別にいいんですけど、Fireした時の処理が「// FIRE ZE MISSILES!」というコメントしかない。違うそうじゃない呼び出し元に結果を返してほしいんだと。
で、ネットを探してみても、「//OK時の処理をここに書く」みたいなもので終わっているものが多く、望むものが見つからなかったので私がまとめてみました。

題して「汎用YesNoダイアログ」です。

# Activityから呼ぶ汎用YesNoダイアログ

#### 環境

android studio APIレベル32 (java)

## 動作イメージ

呼び出し元ができるだけシンプルになりますように、というのがポリシーです。

なので呼び出し元のMainActivityから考えてみます。前回でてきた`startActivityForResult`の考え方、requestCodeを渡して、それで判断する、というような形式が分かりやすいと感心したので、それを真似てみます。

この汎用YesNoダイアログの名前を`CommonDialog`としています。
呼び出しは一行で済ませたいです。

~~~java
CommonDialog.openDialog(getSupportFragmentManager(), 1,
         "削除してもいいですか？", "OK", "Cancel", null);
~~~

OpenDialog内の静的メソッドをキックします。引数は、表示に必要なFragmentManager。任意の数字requestCode。そして、メッセージ文言とOK、キャンセル、その他ボタンの表示名です。キャンセルとその他はnullを指定すると非表示になります。

受け取りはイベントで行いたいです。`onCloseCommonDialog`として受けます。

~~~java
public void onCloseCommonDialog(int requestCode, int responseCode){
    Log.d("myApp", "requestCode:"+ requestCode + "/ responseCode:" + responseCode);
};
~~~
requestCodeは呼び出し時に指定した数字。responseCodeはOKやCancelが返ってくる。
これで何のダイアログが終了し、何が帰ってきたかを分岐することができます。

では実装‥‥‥

##Dialog実装

package宣言以外を全部貼ります。解説はのちほど。

~~~java
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;

import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;

public class CommonDialog  extends DialogFragment {
    /** YESボタン */
    public static final int BUTTON_POSITIVE = -1;
    /** NOボタン */
    public static final int BUTTON_NEGATIVE = -2;
    /** その他ボタン */
    public static final int BUTTON_NEUTRAL = -3;

    /** Dialog終了イベントリスナ */
    public interface OnCloseCommonDialogListener {
        void onCloseCommonDialog(int requestCode, int responseCode);
    }

    /** ダイアログ呼び出し 静的メソッド*/
    public static void openDialog(FragmentManager fm ,
                                  int requestCode,
                                  String message,
                                  String labelPositive,
                                  String labelNegative,
                                  String labelNeutral){
        DialogFragment dialogFragment = new CommonDialog();
        Bundle args = new Bundle();
        args.putInt("requestCode", requestCode);
        args.putString("message", message);
        args.putString("labelPositive", labelPositive);
        args.putString("labelNegative", labelNegative);
        args.putString("labelNeutral", labelNeutral);
        dialogFragment.setArguments(args);
        dialogFragment.show(fm, "commonDialog-" + requestCode);
    }

    /** ダイアログのクリックを受け取る内部リスナ */
    private class OnDialogClickListener implements DialogInterface.OnClickListener
    {
        private final int requestCode;
        private final int responseCode;
        // コンストラクタ
        public OnDialogClickListener(int requestCode, int responseCode){
            this.requestCode = requestCode;
            this.responseCode = responseCode;
        }
        // クリックイベント
        @Override
        public void onClick(DialogInterface dialogInterface, int i) {
            listener.onCloseCommonDialog(this.requestCode, this.responseCode);
        }
    }

    /** Dialogの生成 */
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // 引数取得
        int requestCode      = getArguments().getInt("requestCode",0);
        String message       = getArguments().getString("message","");
        String labelPositive = getArguments().getString("labelPositive","OK");
        String labelNegative = getArguments().getString("labelNegative",null);
        String labelNeutral  = getArguments().getString("labelNeutral",null);
        // ダイアログ作成開始
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setMessage(message);
        builder.setPositiveButton(labelPositive,
            new OnDialogClickListener(requestCode, BUTTON_POSITIVE));
        if(labelNegative != null){
            builder.setNegativeButton(labelNegative,
            new OnDialogClickListener(requestCode, BUTTON_NEGATIVE));
        }
        if(labelNeutral != null){
            builder.setNeutralButton(labelNeutral,
            new OnDialogClickListener(requestCode, BUTTON_NEUTRAL));
        }
        return builder.create();
    }

    /** Dialog終了イベントリスナ：実体 */
    private OnCloseCommonDialogListener listener;

    /** Dialog終了イベントリスナをActivityのイベントに紐づけ */
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        try {
            listener = (OnCloseCommonDialogListener) context;
        } catch (ClassCastException e) {
            throw new ClassCastException("activity must implement Listener");
        }
    }
}
~~~
うぅい。

##呼出し側実装

上述のDialogを呼びます。

**インプリメント**

~~~java
public class MainActivity extends AppCompatActivity
             implements CommonDialog.OnCloseCommonDialogListener{
 // ‥‥
~~~

このActivityは、CommonDialogの`onCloseCommonDialog`イベントに対応し、結果をこれで受け取ります。という宣言。

**呼び出し**

~~~java
    private static final int REQUEST_DIALOG_DELETE = 1;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
         // ‥‥元からある処理
         binding.btnDelete.setOnClickListener( v -> {
            CommonDialog.openDialog(getSupportFragmentManager(),
                    REQUEST_DIALOG_DELETE,"削除してもいいですか？",
                    "OK", "Cancel", null);
        });
    }
~~~
必要なボタンなどからCommonDialogを呼びます。呼び方は上述したとおり。
ここではデータの削除するためのボタンのイメージで。

**受け取り**

~~~java
    //ダイアログが閉じた処理
    public void onCloseCommonDialog(int requestCode, int responseCode){
        Log.d("myApp", "requestCode:"+ requestCode + "/ responseCode:" + responseCode);
        if(requestCode == REQUEST_DIALOG_DELETE){
            switch (responseCode){
                case CommonDialog.BUTTON_POSITIVE:
                    // OK時処理実行
                    break;
                case CommonDialog.BUTTON_NEGATIVE:
                    // 取消実行
                    break;
            }
        }
    };
~~~
requestCodeとresponseCodeから判定、処理を書きます。ここはActivty内なので、thisメソッドやthisフィールドに届きます。
削除であればActivityがフィールドで持っているデータを消すことができますね。

## 実装内容の解説
さて、解説をメモしていきます。コードの順番は入れ替えています。

~~~java
public class CommonDialog extends DialogFragment {
~~~
DialogFragmentを継承します。DialogFragment は `android.app.DialogFragment`と`androidx.fragment.app.DialogFragment`の二つがあります。後者の方が新しいのでしょう、たぶん‥‥。

~~~java
    /** Dialog終了イベントリスナ */
    public interface OnCloseCommonDialogListener {
        void onCloseCommonDialog(int requestCode, int responseCode);
    }
~~~

これが呼び出し元であるActivityが実装すべきイベントリスナです。

~~~java
    /** Dialog終了イベントリスナ：実体 */
    private OnCloseCommonDialogListener listener;

    /** Dialog終了イベントリスナをActivityのイベントに紐づけ */
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        try {
            listener = (OnCloseCommonDialogListener) context;
        } catch (ClassCastException e) {
            throw new ClassCastException("activity must implement Listener");
        }
    }
~~~

`onAttach`イベントで親Activityと接触した際に、親Activity本体を`listener`に保持します。
親Activityは`OnCloseCommonDialogListener`を実装しているのでこれが可能になります。

~~~java
    /** ダイアログのクリックを受け取る内部リスナ */
    private class OnDialogClickListener implements DialogInterface.OnClickListener
    {
        private final int requestCode;
        private final int responseCode;
        // コンストラクタ
        public OnDialogClickListener(int requestCode, int responseCode){
            this.requestCode = requestCode;
            this.responseCode = responseCode;
        }
        // クリックイベント
        @Override
        public void onClick(DialogInterface dialogInterface, int i) {
            listener.onCloseCommonDialog(this.requestCode, this.responseCode);
        }
    }
~~~
これはfragment内で呼ばれるイベントリスナです。privateであり、Activityからは水面下の実装です。

通常呼ばれる`DialogInterface.OnClickListener`にフィールドを追加したいのでこうしています。
`onClick`イベントがCommonDialog内部で発生し、それが親Activityの`onCloseCommonDialog`として伝播するようになっています。
（private class内からCommonDialogのフィールドであるlistenerが呼べるのは少し違和感が。なんででしたっけ‥‥？）

~~~java
    /** ダイアログ呼び出し 静的メソッド*/
    public static void openDialog(FragmentManager fm ,
                                  int requestCode,
                                  String message,
                                  String labelPositive,
                                  String labelNegative,
                                  String labelNeutral){
        DialogFragment dialogFragment = new CommonDialog();
        Bundle args = new Bundle();
        args.putInt("requestCode", requestCode);
        args.putString("message", message);
        args.putString("labelPositive", labelPositive);
        args.putString("labelNegative", labelNegative);
        args.putString("labelNeutral", labelNeutral);
        dialogFragment.setArguments(args);
        dialogFragment.show(fm, "commonDialog-" + requestCode);
    }
~~~

呼び出すための静的メソッドです。Bundleの中に引数を詰め込んでshowします。

この手の呼び出しロジックをネットで調べると、この手の処理はActivity側で書くことが多いみたいなんですけど、それだと引数名の文字列(`"labelPositive"`)やその数とかをお互いで共有しないとならないので、煩雑ではないですか？
呼び出される側が静的メソッドを用意して引数で受けた方がスッキリすると思うんですけど、どうでしょうか‥‥。

ベストプラクティスを知りたいところ。

~~~java
    /** Dialogの生成 */
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // 引数取得
        int requestCode      = getArguments().getInt("requestCode",0);
        String message       = getArguments().getString("message","");
        String labelPositive = getArguments().getString("labelPositive","OK");
        String labelNegative = getArguments().getString("labelNegative",null);
        String labelNeutral  = getArguments().getString("labelNeutral",null);
        // ダイアログ作成開始
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setMessage(message);
        builder.setPositiveButton(labelPositive, new OnDialogClickListener(requestCode, BUTTON_POSITIVE));
        if(labelNegative != null){
            builder.setNegativeButton(labelNegative, new OnDialogClickListener(requestCode, BUTTON_NEGATIVE));
        }
        if(labelNeutral != null){
            builder.setNeutralButton(labelNeutral, new OnDialogClickListener(requestCode, BUTTON_NEUTRAL));
        }
        return builder.create();
    }
~~~

生成されたCommonDialog内で走る処理。引数を取ってくるのと、各ボタンに動作を割り当てるボタン。
３つのボタンとも、`DialogInterface.OnClickListener`を設定する所ですが、それだとどのボタンを押したかわからなくなってしまうので、`DialogInterface.OnClickListener`を兼ねる`OnDialogClickListener`を作ってそれを指定し、その引数で区別できるようにしています。

----
たぶんこういうイベントの繋ぎ変えみたいなことをしなくても、
DialogInterfaceのonClickの引数、DialogInterfaceあたりをうまくすればうまくいくのかもしれません。
いろいろ首をひねるところではあるのですけど、呼び出し元がスッキリ書けるようになったので個人的には満足です。

メンターさんのご意見を伺いたいところ。

----
独学のため正確でない可能性があります。
（っ・ｘ・）っ きゅ

#### 参考
- https://developer.android.com/guide/topics/ui/dialogs?hl=ja