画面回転後もコールバックリスナーが途切れないDialogFragmentを作る

これは画面回転等で発生するActivity再生成後もコールバックを保ち続けられる`DialogFragment`を出来るだけシンプルに実装できるよう汎用的な抽象ダイアログクラスを作ることを目指して試行錯誤した記事です。
※かなりオレオレ要素が入っているので最適解ではありません！

## 前提条件
前提として以下を挙げます。

#### (a). `android.app.Dialog`は`Activity`等のコントローラから直接呼び出さない
Android2.xまでの`Dialog`を直接呼び出す記述は4.x以降非推奨となっています。
メモリリークしてしまう場合があるので`DialogFragment`を使います。

#### (b). `DialogFragment`上で処理を書かない（コールバックさせる）
`DialogFragmnet`上ではユーザーの入力のみを受け付け、その結果を元のコントローラにコールバックしてそこで処理を書くようにします。
ボタンを配置してタップイベントをリスナーで受け取るのと同じです。
`DialogFragment`上にタップイベントの処理を書いたり、`getActivity()`をキャストしてメソッドをコールするのは依存度を上げてしまい好ましくないです

#### (c). `Activity`再生成に対応する
画面回転やメモリ不足等で`Activity`の再生成が発生すると元の`Activity`と`DialogFragment`自身が再生成されます。
問題はコールバックリスナーを設定していた場合、再生成した両者を再び繋ぎ直さないといけません。

これらを事柄を守りつつ実装クラス側でなるべく簡素に書けるように抽象クラスを組むのに挑戦してみます。

##  1. `DialogFragment`抽象クラス
コントローラへコールバックさせる関係上インターフェースを実装しますが、
抽象クラス側ではどのようなインターフェースを実装するか判断できないためジェネリクスで宣言します。
実装クラス側で`<Interface>`にインターフェースクラスを指定させる感じです。

```java:BaseCallbackDialog.java
public abstract class BaseCallbackDialog<Interface> extends DialogFragment {
 ...
}
```

またコールバックさせるためにコントローラから`Listener`をセットさせる口を作ります。

```java:BaseCallbackDialog.java

public abstract class BaseCallbackDialog<Interface> extends DialogFragment {

    private Interface mListener;
    
    public Interface getCallbackListener() {
        return mListener;
    }

    public void setCallbackListener(Interface listener) {
        mListener = listener;
    }

}
```

しかしながらこれでは`Activiy`が再生成するとセットした`mListener`が初期化されるため、コールバックしなくなってしまいます。
そこで再接続をさせるのですが、インターフェースを実装した箇所によって再接続が可能かどうか分かれ、それぞれ再接続方法も異なります。

分ける区分としてインターフェースをコントローラ側で
(a). `Activity`自体に実装(`implements`)した場合
(b). `Fragment`自体に実装(`implements`)した場合
(c). 匿名クラスなど、それ以外で実装した場合
の3種類で分けてみます。

(a)、(b)については再接続が可能です。
(c)については(抽象クラス側で)再接続させることは難しいです。

## 2. `BaseCallbackDialog#setCallbackListener`を書き直す
上記の３つの区分に分けて`BaseCallbackDialog#setCallbackListener`を書きなおしてみます。

```java:BaseCallbackDialog.java

public abstract class BaseCallbackDialog<Interface> extends DialogFragment {

    private enum ListenerType {
        ACTIVITY,
        FRAGMENT,
        OTHER,
    }

    private final static String ARG_LISTENER_TYPE = "listenerType";

    public void setCallbackListener(Interface listener) {
        mListener = listener;

        // リスナーのタイプをチェック
        ListenerType listenerType;
        if (listener == null) {
            //nullの場合
            listenerType = null;
            setTargetFragment(null, 0);
        } else if (listener instanceof Activity) {
            //Activityの場合
            listenerType = ListenerType.ACTIVITY;
            setTargetFragment(null, 0);
        } else if (listener instanceof Fragment) {
            //Fragmentの場合
            listenerType = ListenerType.FRAGMENT;
            setTargetFragment((Fragment) listener, 0);
        } else {
            //その他の場合
            listenerType = ListenerType.OTHER;
            setTargetFragment(null, 0);
        }

        // 取得したリスナーのタイプをBundleに持たせておく
        Bundle bundle = getArguments();
        bundle.putSerializable(ARG_LISTENER_TYPE, listenerType);
        setArguments(bundle);
    }

    ...

}
```

インスタンスの種類で分岐させてタイプをセットします。
`Fragment`の場合には`Fragment#setTargetFragment`にもFragment自身をセットします。
最後にタイプを`Bundle`に持たせることで`Activity`再生成後もどのタイプを保持していたか記憶できるようにしています。

## 3. `Fragment#onAttach`に再接続処理を記載する
再生成が発生した場合は`Fragment#onAttach`が呼ばれるのでそこで再接続の処理を記載します。

```java:BaseCallbackDialog.java
public abstract class BaseCallbackDialog<Interface> extends DialogFragment {

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);

        Bundle bundle = getArguments();
        ListenerType listenerType = (ListenerType) bundle.getSerializable(ARG_LISTENER_TYPE);

        if (listenerType == ListenerType.ACTIVITY) mListener = (Interface) activity;
        else if (listenerType == ListenerType.FRAGMENT) mListener = (Interface) getTargetFragment();
    }

    ...

}
```

`Activity`か`Fragment`に実装されている場合はそれぞれ引数や`Fragment#getTargetFragment`から取得できるので`mListener`にセットし直します。
その他に実装されている場合はどうしようもないので無視しています。

場合によっては無視せず`BaseCallbackDialog#setCallbackListenter`にActivityか`Fragment`以外が入ってきたら`IllegalArgumentException`辺りを吐いてしまうのも良いかもしれませんね。

ここまで実装すれば抽象クラスは終わりです！
というわけで使ってみます。

## 4. `DialogFragment`実装クラス
上記の抽象クラスを用いて`DialogFragment`を実装すると以下のようになります。
今回は簡単な確認ダイアログを表示してみます。

```java:ConfirmDialog.java

public class ConfirmDialog extends BaseCallbackDialog<ConfirmDialog.DialogCallbackListener> {

    public interface DialogCallbackListener {
        void onPositiveButtonClicked();
        void onNegativeButtonClicked();
    }

    public static ConfirmDialog newInstance() {
        ConfirmDialog dialog = new ConfirmDialog();
        return dialog;
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setTitle("確認");
        builder.setMessage("よろしいですか？");
        builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                if (getCallbackListener() != null) getCallbackListener().onPositiveButtonClicked();
            }
        });
        builder.setNegativeButton("キャンセル", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                if (getCallbackListener() != null) getCallbackListener().onNegativeButtonClicked();
            }
        });
        Dialog dialog = builder.create();
        return dialog;
    }
}
```
抽象クラスでインターフェース周りを記載しているのでこれで十分です。
`Activity`か`Fragment`にコールバックを実装していれば画面回転させてもコールバックインターフェースが切れることはありません。
任意のタイミングで`BaseCallbackDialog#getCallbackListener()`から呼び出し元コントローラに処理を返して下さい。

## 5. コントローラクラス側の実装
あとはコントローラクラス側で呼び出すだけです。

```java:MainActivity.java
public class MainActivity extends Activity implements ConfirmDialog.DialogCallbackListener {

    ...

    private void showConfirmDialog(){
        ConfirmDialog dialog = ConfirmDialog.newInstance();
        dialog.setCallbackListener(this);
        dialog.show(getSupportFragmentManager(), "");
    }

    /* ConfirmDialog.DialogCallbackListener */

    @Override
    public void onPositiveButtonClicked() {
        ...
    }

    @Override
    public void onNegativeButtonClicked() {
        ...
    }

}
```

## おわり
上記の抽象クラスを用いた実装であれば
・`Activity`か`Fragment`に`implement`させること（匿名クラスではダメ）
・インターフェースは１ダイアログに一つしか持てない
など制約はありますがそれぞれの`DialogFragment`で`Activity`再生成処理を気にすることなくコールバックを実装することが可能です。

抽象クラスのコードの全文は以下のGistに置いてあります。
https://gist.github.com/KazaKago/76eac08fe048a8a3e4a9

## 補足
今回のクラスはJavaインタフェースによる標準的なコールバックを用いましたが、このほかにも[EventBus](https://github.com/greenrobot/EventBus)などを用いて通知を受け取るのも有効な手段です。