LoginSignup
26
24

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-07-31

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

前提条件

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

(a). android.app.DialogActivity等のコントローラから直接呼び出さない

Android2.xまでのDialogを直接呼び出す記述は4.x以降非推奨となっています。
メモリリークしてしまう場合があるのでDialogFragmentを使います。

(b). DialogFragment上で処理を書かない(コールバックさせる)

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

(c). Activity再生成に対応する

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

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

1. DialogFragment抽象クラス

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

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

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

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を書き直す

上記の3つの区分に分けてBaseCallbackDialog#setCallbackListenerを書きなおしてみます。

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が呼ばれるのでそこで再接続の処理を記載します。

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

    ...

}

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

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

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

4. DialogFragment実装クラス

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

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;
    }
}

抽象クラスでインターフェース周りを記載しているのでこれで十分です。
ActivityFragmentにコールバックを実装していれば画面回転させてもコールバックインターフェースが切れることはありません。
任意のタイミングでBaseCallbackDialog#getCallbackListener()から呼び出し元コントローラに処理を返して下さい。

5. コントローラクラス側の実装

あとはコントローラクラス側で呼び出すだけです。

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() {
        ...
    }

}

おわり

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

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

補足

今回のクラスはJavaインタフェースによる標準的なコールバックを用いましたが、このほかにもEventBusなどを用いて通知を受け取るのも有効な手段です。

26
24
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
24