DialogFragment 実装パターン

DialogFragment がやや癖があるのは [拙記事: DialogFragment](http://qiita.com/kojionilk/items/71869b4b51f1591cdd7a) で書いた。Dialog を表示する度に毎回インナー static クラスを定義するのは辛い。何とか省力化を図りつつそれなりに汎用性のある実装パターンはどうすれば良いのか、というのが今回の話題である。

一応前提として AppCompatActivity (Support Library) を使用しているので `getParentFragment()` や `getChildFragmentManager()` は常にあることとする。

# onActivityResult() で返すパターン

前 Qiita に DialogFragment の結果を `onActivityResult()` で返す実装パターンの記事が載っておりそれは Activity で出す場合と Fragment で出す場合の双方に対応していた。当時成る程と思い一つのプロジェクトでこのパターンで実装してみた。

Fragment を使い出すと正直 Activity で Dialog を出すのはほとんど発生しないので Fragment 決め打ちでも良くて、例えば以下の様なコードで親 Fragment に処理を返す:

```java
public final class MyDialogFragment extends DialogFragment {
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        final DialogInterface.OnClickListener listener = (dialog, which) -> {
            dismiss();
            Intent intent = new Intent();
            if (getArguments() != null) {
                intent.putExtras(getArguments());  // intent に Dialog 実行時の引数を詰める
            }
            getParentFragment().onActivityResult(getTargetRequestCode(), which, intent);
        };
        return new AlertDialog.Builder(getActivity())
                .setPositiveButton("OK", listener)
                .setNegativeButton("キャンセル", listener)
                .create();
    }
}
```

親 Fragment 側では以下で呼び出す:

```java
public final class MyFragment extends Fragment {
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        final MyDialogFragment dialog = new MyDialogFragment();
        dialog.setTargetFragment(this, 100);
        dialog.setArguments(bundle("aaa", "bbb", "ccc", "ddd"));
        dialog.show(getChildFragmentManager(), "my_dialog");
    }

    @Override
    public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
        if (requestCode == 100) {
            if (resultCode == DialogInterface.BUTTON_POSITIVE) {
                // positive_button 押下時の処理
            } else if (resultCode == DialogInterface.BUTTON_NEGATIVE) {
                // negative_button 押下時の処理
            }
        }
    }
}
```

これの利点は **余計な Interface を用意したり実装しなくていい** ということだ。あと requestCode も `setTargetFragment()` する時のものがそのまま `onActivityResult()` で返せるのでしっくりくる。`onActivityResult()` の resultCode で実行結果を返せるが DialogInterface の BUTTON_POSITIVE, BUTTON_NEGATIVE, BUTTON_NEUTRAL で -1, -2, -3 が予約されているのでそれを返却するようにすれば何が押されたかがわかるし、リストを表示している場合は position (>=0) を返してやればどれが選択されたかがわかる。

ただやっぱり個人的には以下が気になる。気にならないのなら十分アリだと思った:

* 若干バッドノウハウな気がする
* `onActivityResult()` に Dialog の処理と本来の `onActivityResult()` の処理が混じる (Fragment や Activity から結果が返された時の処理 ... まぁ DialogFragment も Fragment なのだが)
* `android.app.Activity#onActivityResult()` が **protected なので普通に呼べない**
* うまく共通化してあれば `onActivityResult()` に書くのと共通コールバック実装するのはさほどコード量が変わらない

# Builder パターンで共通化

そんなわけで毎度おなじみの Builder パターンで共通化してみた。以下のケースに対応する:

* AppCompatActivity 若しくは Fragment 上でダイアログを表示
* タイトル, メッセージ, リスト, 肯定ボタン, 否定ボタン, 任意のパラメータ渡し, キャンセル可能か否か, キャンセル時のコールバック

[拙記事: DialogFragment](http://qiita.com/kojionilk/items/71869b4b51f1591cdd7a) に書いたとおり `DialogFragment#setArguments()` で詰める Bundle に入るものでないと DialogFragment が破棄された時に状態復帰できないので共通化できる項目はどうしても限られてしまう。

また AlertDialog をインポートする時は android.app.AlertDialog でなく android.support.v7.app.AlertDialog を使うのが良い。マテリアルデザインになる。

```java
public final class MyDialogFragment extends DialogFragment {

    /**
     * MyDialog で何か処理が起こった場合にコールバックされるリスナ.
     */
    public interface Callback {

        /**
         * MyDialog で positiveButton, NegativeButton, リスト選択など行われた際に呼ばれる.
         *
         * @param requestCode MyDialogFragment 実行時 requestCode
         * @param resultCode DialogInterface.BUTTON_(POSI|NEGA)TIVE 若しくはリストの position
         * @param params MyDialogFragment に受渡した引数
         */
        void onMyDialogSucceeded(int requestCode, int resultCode, Bundle params);

        /**
         * MyDialog がキャンセルされた時に呼ばれる.
         *
         * @param requestCode MyDialogFragment 実行時 requestCode
         * @param params MyDialogFragment に受渡した引数
         */
        void onMyDialogCancelled(int requestCode, Bundle params);
    }

    /**
     * MyDialogFragment を Builder パターンで生成する為のクラス.
     */
    public static class Builder {

        /** Activity. */
        final AppCompatActivity mActivity;

        /** 親 Fragment. */
        final Fragment mParentFragment;

        /** タイトル. */
        String mTitle;

        /** メッセージ. */
        String mMessage;

        /** 選択リスト. */
        String[] mItems;

        /** 肯定ボタン. */
        String mPositiveLabel;

        /** 否定ボタン. */
        String mNegativeLabel;

        /** リクエストコード. 親 Fragment 側の戻りで受け取る. */
        int mRequestCode = -1;

        /** リスナに受け渡す任意のパラメータ. */
        Bundle mParams;

        /** DialogFragment のタグ. */
        String mTag = "default";

        /** Dialog をキャンセル可かどうか. */
        boolean mCancelable = true;

        /**
         * コンストラクタ. Activity 上から生成する場合.
         *
         * @param activity
         */
        public <A extends AppCompatActivity & Callback> Builder(@NonNull final A activity) {
            mActivity = activity;
            mParentFragment = null;
        }

        /**
         * コンストラクタ. Fragment 上から生成する場合.
         *
         * @param parentFragment 親 Fragment
         */
        public <F extends Fragment & Callback> Builder(@NonNull final F parentFragment) {
            mParentFragment = parentFragment;
            mActivity = null;
        }

        /**
         * タイトルを設定する.
         *
         * @param title タイトル
         * @return Builder
         */
        public Builder title(@NonNull final String title) {
            mTitle = title;
            return this;
        }

        /**
         * タイトルを設定する.
         *
         * @param title タイトル
         * @return Builder
         */
        public Builder title(@StringRes final int title) {
            return title(getContext().getString(title));
        }

        /**
         * メッセージを設定する.
         *
         * @param message メッセージ
         * @return Builder
         */
        public Builder message(@NonNull final String message) {
            mMessage = message;
            return this;
        }

        /**
         * メッセージを設定する.
         *
         * @param message メッセージ
         * @return Builder
         */
        public Builder message(@StringRes final int message) {
            return message(getContext().getString(message));
        }

        /**
         * 選択リストを設定する.
         *
         * @param items 選択リスト
         * @return Builder
         */
        public Builder items(@NonNull final String... items) {
            mItems = items;
            return this;
        }

        /**
         * 肯定ボタンを設定する.
         *
         * @param positiveLabel 肯定ボタンのラベル
         * @return Builder
         */
        public Builder positive(@NonNull final String positiveLabel) {
            mPositiveLabel = positiveLabel;
            return this;
        }

        /**
         * 肯定ボタンを設定する.
         *
         * @param positiveLabel 肯定ボタンのラベル
         * @return Builder
         */
        public Builder positive(@StringRes final int positiveLabel) {
            return positive(getContext().getString(positiveLabel));
        }

        /**
         * 否定ボタンを設定する.
         *
         * @param negativeLabel 否定ボタンのラベル
         * @return Builder
         */
        public Builder negative(@NonNull final String negativeLabel) {
            mNegativeLabel = negativeLabel;
            return this;
        }

        /**
         * 否定ボタンを設定する.
         *
         * @param negativeLabel 否定ボタンのラベル
         * @return Builder
         */
        public Builder negative(@StringRes final int negativeLabel) {
            return negative(getContext().getString(negativeLabel));
        }

        /**
         * リクエストコードを設定する.
         *
         * @param requestCode リクエストコード
         * @return Builder
         */
        public Builder requestCode(final int requestCode) {
            mRequestCode = requestCode;
            return this;
        }

        /**
         * DialogFragment のタグを設定する.
         *
         * @param tag タグ
         * @return Builder
         */
        public Builder tag(final String tag) {
            mTag = tag;
            return this;
        }

        /**
         * Positive / Negative 押下時のリスナに受け渡すパラメータを設定する.
         *
         * @param params リスナに受け渡すパラメータ
         * @return Builder
         */
        public Builder params(final Bundle params) {
            mParams = new Bundle(params);
            return this;
        }

        /**
         * Dialog をキャンセルできるか否かをセットする.
         *
         * @param cancelable キャンセル可か否か
         * @return Builder
         */
        public Builder cancelable(final boolean cancelable) {
            mCancelable = cancelable;
            return this;
        }

        /**
         * DialogFragment を Builder に設定した情報を元に show する.
         */
        public void show() {
            final Bundle args = new Bundle();
            args.putString("title", mTitle);
            args.putString("message", mMessage);
            args.putStringArray("items", mItems);
            args.putString("positive_label", mPositiveLabel);
            args.putString("negative_label", mNegativeLabel);
            args.putBoolean("cancelable", mCancelable);
            if (mParams != null) {
                args.putBundle("params", mParams);
            }

            final MyDialogFragment f = new MyDialogFragment();
            if (mParentFragment != null) {
                f.setTargetFragment(mParentFragment, mRequestCode);
            } else {
                args.putInt("request_code", mRequestCode);
            }
            f.setArguments(args);
            if (mParentFragment != null) {
                f.show(mParentFragment.getChildFragmentManager(), mTag);
            } else {
                f.show(mActivity.getSupportFragmentManager(), mTag);
            }
        }

        /**
         * コンテキストを取得する. getString() 呼び出しの為.
         *
         * @return Context
         */
        private Context getContext() {
            return (mActivity == null ? mParentFragment.getActivity() : mActivity).getApplicationContext();
        }
    }

    /** Callback. */
    private Callback mCallback;

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        Object callback = getParentFragment();
        if (callback == null) {
            callback = getActivity();
            if (callback == null || !(callback instanceof Callback)) {
                throw new IllegalStateException();
            }
        }
        mCallback = (Callback) callback;
    }

    @Override
    public void onDetach() {
        super.onDetach();
        mCallback = null;
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        final DialogInterface.OnClickListener listener = (dialog, which) -> {
            dismiss();
            mCallback.onMyDialogSucceeded(getRequestCode(), which, getArguments().getBundle("params"));
        };
        final String title = getArguments().getString("title");
        final String message = getArguments().getString("message");
        final String[] items = getArguments().getStringArray("items");
        final String positiveLabel = getArguments().getString("positive_label");
        final String negativeLabel = getArguments().getString("negative_label");
        setCancelable(getArguments().getBoolean("cancelable"));
        final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        if (!TextUtils.isEmpty(title)) {
            builder.setTitle(title);
        }
        if (!TextUtils.isEmpty(message)) {
            builder.setMessage(message);
        }
        if (items != null && items.length > 0) {
            builder.setItems(items, listener);
        }
        if (!TextUtils.isEmpty(positiveLabel)) {
            builder.setPositiveButton(positiveLabel, listener);
        }
        if (!TextUtils.isEmpty(negativeLabel)) {
            builder.setNegativeButton(negativeLabel, listener);
        }
        return builder.create();
    }

    @Override
    public void onCancel(DialogInterface dialog) {
        mCallback.onMyDialogCancelled(getRequestCode(), getArguments().getBundle("params"));
    }

    /**
     * リクエストコードを取得する. Activity と ParentFragment 双方に対応するため.
     *
     * @return requestCode
     */
    private int getRequestCode() {
        return getArguments().containsKey("request_code") ? getArguments().getInt("request_code") : getTargetRequestCode();
    }
}
```

Activity 上で以下のように表示できる:

```java
public final class MyActivity extends AppCompatActivity implements MyDialogFragment.Callback {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new MyDialogFragment.Builder(this)
                .title("title")
                .message("message")
                .requestCode(111)
                .params(bundle("aaa", "bbb", "ccc", "ddd"))
                .positive("OK")
                .negative("キャンセル")
                .show();
    }

    @Override
    public void onMyDialogSucceeded(int requestCode, int resultCode, Bundle params) {
        Log.d("MyDialog", "succeeded. requestCode: " + requestCode + "; resultCode: " + resultCode + "; params: " + params.toString());
    }

    @Override
    public void onMyDialogCancelled(int requestCode, Bundle params) {
        Log.d("MyDialog", "cancelled. requestCode: " + requestCode + "; params: " + params.toString());
    }
}
```

```
D/MyDialog(12870): succeeded. requestCode: 111; resultCode: -1; params: Bundle[{aaa=bbb, ccc=ddd}]
D/MyDialog(13141): cancelled. requestCode: 111; params: Bundle[{aaa=bbb, ccc=ddd}]
```

Fragment 上でも同様に以下のように表示する:

```java
public final class MyFragment extends Fragment implements MyDialogFragment.Callback {
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        new MyDialogFragment.Builder(this)
                .title("title")
                .message("message")
                .requestCode(111)
                .params(bundle("aaa", "bbb", "ccc", "ddd"))
                .positive("OK")
                .negative("キャンセル")
                .show();
    }
}
```

# この共通 Dialog で賄えないものはどうするのか?

**諦めてインナー static クラスで適宜定義するのが良い。** ほとんどの場合タイトル、メッセージ、OK キャンセルボタンだったり、ちょっとした選択肢 (リスト) を表示したりとかで賄えるはずなので、面倒くさい定義はほとんど行わなくてよいはず。あと良く使うのは `setAdapter()` して任意のリスト表示を行うものだが Adapter が Parcelable でないので Bundle に詰められない (= 画面回転などで状態が消えてしまう) ので共通化できない。`setView()` するものも同様。