DialogFragmentの結果をホストに渡すための抽象クラス


DialogFragmentの実装は、なかなか面倒です。
先人達のプラクティスは色々ありますが、それらを集約させるにも経験が必要です。
そのためなのか (少なくとも自分が出会う) ダイアログに関する実装は低品質になりがち。と感じます。

ここでは DialogFragmentの結果をホストに渡す機能を提供する抽象クラスを紹介します。
そして ニーズの高い「単純なメッセージ形式のAlertDialog」を表示させる実装クラス例なども紹介します。

各自のDialogFragmentを実装する際の出発点として利用いただければ。と思います。

## ソースコード＆デモアプリ

記事中のソースコードは抜粋した状態で紹介しています。
完全なソースコードとデモは、以下のリポジトリを参照してください。

https://github.com/nukka123/DialogFragmentDemo


## AbstractDialogFragment

DialogFragmentの結果をホストに渡す機能を提供する抽象クラスとして、AbstractDialogFragmentクラスを作成しました。この抽象クラスではダイアログの生成処理を実装クラスに委譲します。実装クラスにて各々のダイアログを生成してもらいます。

後述する実装クラス例のAlertDialogFragmentでは、以下のようなメソッド・チェーンでダイアログを表示させる事ができます。

```java
public class MainFragment extends Fragment implements AbstractDialogFragment.Callback {
・・・
    void showAlertDialog() {
        new AlertDialogFragment.Builder()
                .setIcon(android.R.drawable.ic_dialog_info)
                .setTitle(R.string.alert_title)
                .setMessage(R.string.alert_message)
                .setPositiveButton(R.string.alert_ok)
                .setNeutralButton(R.string.alert_cancel)
                .setNegativeButton(R.string.alert_ng)
                .build(REQ_ID_ALERT_DIALOG)
                .showOn(this, DIALOG_TAG);
    }
```


ダイアログの結果を渡すための AbstractDialogFragment.Callback インタフェースを用意しています。

```java
public abstract class AbstractDialogFragment extends DialogFragment {
・・・
    public interface Callback {
        void onDialogResult(int requestCode, int resultCode, Intent data);
        void onDialogCancelled(int requestCode);
    }
```

ダイアログの利用者は、アクティビティ 又は フラグメントにて AbstractDialogFragment.Callback を実装します。onActivityResult()の実装と同様に、requestCodeでダイアログを識別して、結果受信後の処理を実装するようにしてください。

```java
public class MainFragment extends Fragment implements AbstractDialogFragment.Callback {
・・・
    @Override
    public void onDialogResult(int requestCode, int resultCode, Intent data) {
        // ダイアログのボタン押下時の処理
    }

    @Override
    public void onDialogCancelled(int requestCode) {
        // ダイアログのキャンセル時の処理
    }
```

AbstractDialogFragmentでは、独自の3種の表示メソッドを用意しています.
ダイアログの利用者は、これら表示メソッドを使用することで結果の通知先となる host を指定します。

```java
public abstract class AbstractDialogFragment extends DialogFragment {
・・・
    // ダイアログを表示する。結果はhostのアクティビティへ通知される。
    public void showOn(Activity host, String tag) {
        ・・・
        AppCompatActivity hostCompat = (AppCompatActivity) host;
        FragmentManager manager = hostCompat.getSupportFragmentManager();
        super.show(manager, tag);
    }

    // ダイアログを表示する。結果はhostのフラグメントへ通知される。
    public void showOn(Fragment host, String tag) {
        ・・・
        setTargetFragment(host, getArguments().getInt(ARG_REQUEST_CODE));
        FragmentManager manager = host.getFragmentManager();
        super.show(manager, tag);
    }

    // ダイアログを子フラグメントとして表示。結果は親フラグメントとなるhostへ通知される。
    public void showChildOn(Fragment host, String tag) {
        ・・・
        FragmentManager manager = host.getChildFragmentManager();
        super.show(manager, tag);
    }
```

AbstractDialogFragmentでは、実装クラスのために、ダイアログ結果の通知を実施するprotectedメソッドを提供します。実装クラスでは、ダイアログのボタンクリック等で notifyDialogResult()を実行するようにしてください。

このメソッドがAbstractDialogFragmentの実装詳細における肝要部分です。getActivity()、getTargetFragment()、getParentFragment()で取得したインスタンスへ通知するため、アクティビティ/フラグメントが再生成された場合でも、再生成後のインスタンスへ結果を通知する事が可能になります。

```java
public abstract class AbstractDialogFragment extends DialogFragment {
・・・
    protected final void notifyDialogResult(int resultCode, Intent data) {
        Activity activity = getActivity();
        if (shouldCallback(HostType.ACTIVITY) && activity instanceof Callback) {
            Callback callback = (Callback) activity;
            callback.onDialogResult(requestCode, resultCode, data);
        }

        Fragment target = getTargetFragment();
        if (shouldCallback(HostType.TARGET_FRAGMENT) && target instanceof Callback) {
            Callback callback = (Callback) target;
            callback.onDialogResult(requestCode, resultCode, data);
        }

        Fragment parent = getParentFragment();
        if (shouldCallback(HostType.PARENT_FRAGMENT) && parent instanceof Callback) {
            Callback callback = (Callback) parent;
            callback.onDialogResult(requestCode, resultCode, data);
        }
    }
```

他、AbstractDialogFragmentの実装クラスの作成の仕方の詳細は、デモアプリのソースコードとJavadocを参照ください。

## 結果受信後の処理で @OnActivityResult を利用する。

JakeさんのButterknifeライブラリのように、@OnActivityResultを付与することで、onActivityResult()のリクエストコード別処理を簡潔に記述するためのライブラリが幾つか存在します。onDialogResult()の引数形式は、意識的にonActivityResult()と同一にしているため、@OnActivityResultのライブラリの恩恵を受ける事ができます。

vanniktech/OnActivityResult を利用した例は以下のような感じになります。

```java
public class MainFragment extends Fragment implements AbstractDialogFragment.Callback {
・・・
    @Override
    public void onDialogResult(int requestCode, int resultCode, Intent data) {
        ActivityResult.onResult(requestCode, resultCode, data).into(this);
    }

    @OnActivityResult(requestCode = REQ_ID_ALERT_DIALOG)
    void onAlertDialogResult() {
        Timber.d("onAlertDialogResult:");
    }

    @OnActivityResult(requestCode = REQ_ID_DATE_PICKER, resultCodes = DialogInterface.BUTTON_POSITIVE)
    void onDatePickerDialogResult(
            @ExtraInt(name = DatePickerFragment.EXTRA_YEAR) int year,
            @ExtraInt(name = DatePickerFragment.EXTRA_MONTH) int month,
            @ExtraInt(name = DatePickerFragment.EXTRA_DAY) int day) {
        Timber.d("onDatePickerDialogResult: year=%s, month=%s, day=%s", year, month, day);
    }

    @OnActivityResult(requestCode = REQ_ID_LOGIN, resultCodes = DialogInterface.BUTTON_POSITIVE)
    void onLoginDialogResult(
            @ExtraString(name = LoginDialogFragment.EXTRA_USERNAME) String username,
            @ExtraString(name = LoginDialogFragment.EXTRA_PASSWORD) String password) {
        Timber.d("onLoginDialogResult: username=%s, password=%s", username, password);
    }

```

## AlertDialogFragment

色々問題はありましたが、AlertDialog.Builderで直接にダイアログを作成＆表示させていた時代は、蠱惑的なお手軽感がありました。DialogFragmentの導入でダイアログ制御に秩序が生まれましたが、同時にお手軽感が損なわれたと感じます。

先人の2番煎じではありますが、往年のお手軽感を取り戻すべく、AlertDialogFragment 及び そのビルダーを作成しました。

ダイアログ利用者は、以下のようなメソッド・チェーンでダイアログを表示させる事ができます。
※ 記事冒頭のコードを再掲。

```java
public class MainFragment extends Fragment implements AbstractDialogFragment.Callback {
・・・
    void showAlertDialog() {
        new AlertDialogFragment.Builder()
                .setIcon(android.R.drawable.ic_dialog_info)
                .setTitle(R.string.alert_title)
                .setMessage(R.string.alert_message)
                .setPositiveButton(R.string.alert_ok)
                .setNeutralButton(R.string.alert_cancel)
                .setNegativeButton(R.string.alert_ng)
                .build(REQ_ID_ALERT_DIALOG)
                .showOn(this, DIALOG_TAG);
    }
```


もちろんの事。AlertDialogFragmentの構築で与えた各種パラメータは、ダイアログ・フラグメントの再生成で消失させないようにするため、setArguments()で保持するようにしています。

このダイアログ構築時にパラメータを設定させたいDialogFragmentを作る際は、このAlertDialogFragmentの実装を参考にすると良いかと思います。

```java
public class AlertDialogFragment extends AbstractDialogFragment {
・・・
    public static class Builder extends AbstractDialogFragment.Builder {
    ・・・
        @NonNull
        @Override
        protected AbstractDialogFragment build() {
            Bundle args = new Bundle();
            args.putString(ARG_MESSAGE, message);
            args.putString(ARG_TITLE, title);
            args.putString(ARG_NEGATIVE_LABEL, negativeLabel);
            args.putString(ARG_NEUTRAL_LABEL, neutralLabel);
            args.putString(ARG_POSITIVE_LABEL, positiveLabel);
            args.putInt(ARG_MESSAGE_ID, messageId);
            args.putInt(ARG_TITLE_ID, titleId);
            args.putInt(ARG_NEGATIVE_LABEL_ID, negativeLabelId);
            args.putInt(ARG_NEUTRAL_LABEL_ID, neutralLabelId);
            args.putInt(ARG_POSITIVE_LABEL_ID, positiveLabelId);
            args.putInt(ARG_ICON_ID, iconId);

            AbstractDialogFragment dialog = new AlertDialogFragment();
            dialog.setArguments(args);
            return dialog;
        }
```


## DatePickerFragment

AbstractDialogFragmentが面倒な部分を請け負っているため、ピッカー類のダイアログも簡単に実装することができます。開発者サイトの実装例を元に作成した日付ピッカーのダイアログ実装は以下な感じです。

```java
public class DatePickerFragment extends AbstractDialogFragment implements DatePickerDialog.OnDateSetListener {

    public static final String EXTRA_YEAR = "year";
    public static final String EXTRA_MONTH = "month";
    public static final String EXTRA_DAY = "dayOfMonth";

    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        final Calendar c = Calendar.getInstance();
        int year = c.get(Calendar.YEAR);
        int month = c.get(Calendar.MONTH);
        int day = c.get(Calendar.DAY_OF_MONTH);
        return new DatePickerDialog(getContext(), this, year, month, day);
    }

    @Override
    public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) {
        Intent data = new Intent();
        data.putExtra(EXTRA_YEAR, year);
        data.putExtra(EXTRA_MONTH, month);
        data.putExtra(EXTRA_DAY, dayOfMonth);
        notifyDialogResult(DialogInterface.BUTTON_POSITIVE, data);
    }

    public static class Builder extends AbstractDialogFragment.Builder {
        @NonNull
        @Override
        protected AbstractDialogFragment build() {
            return new DatePickerFragment();
        }
    }
}

```


## まとめ

結果をホストに渡す機能を提供するAbstractDialogFragmentと、それを軸としたDialogFragmentの実装例を紹介しました。便利そうと思っていただけたら是使ってみてください。


## 参考
Develop > API Guides > ユーザー インターフェース > ダイアログ
https://developer.android.com/guide/topics/ui/dialogs.html
ダイアログのホストにイベントを渡す
https://developer.android.com/guide/topics/ui/dialogs.html#PassingEvents
Develop > API Guides > User Interface > Input Controls > Pickers
https://developer.android.com/guide/topics/ui/controls/pickers.html
DialogFragment
https://developer.android.com/reference/android/app/DialogFragment.html
Resources
https://developer.android.com/reference/android/content/res/Resources.html
DialogFragment 実装パターン
http://qiita.com/kojionilk/items/9584c012679f61569995
FragmentがネストしているときのsetTargetFragment()は要注意
http://qiita.com/alzybaad/items/23201e45e081d1c8c86f
[Android]DialogFragmentの汎用性を高めたAlertDialogFragment
http://wada811.blogspot.com/2013/05/better-alert-dialog-fragment.html
androidannotations > OnActivityResult
https://github.com/androidannotations/androidannotations/wiki/OnActivityResult
vanniktech/OnActivityResult
https://github.com/vanniktech/OnActivityResult