これは画面回転等で発生する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>
にインターフェースクラスを指定させる感じです。
public abstract class BaseCallbackDialog<Interface> extends DialogFragment {
...
}
またコールバックさせるためにコントローラからListener
をセットさせる口を作ります。
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
を書きなおしてみます。
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
が呼ばれるのでそこで再接続の処理を記載します。
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
を実装すると以下のようになります。
今回は簡単な確認ダイアログを表示してみます。
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. コントローラクラス側の実装
あとはコントローラクラス側で呼び出すだけです。
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
させること(匿名クラスではダメ)
・インターフェースは1ダイアログに一つしか持てない
など制約はありますがそれぞれのDialogFragment
でActivity
再生成処理を気にすることなくコールバックを実装することが可能です。
抽象クラスのコードの全文は以下のGistに置いてあります。
https://gist.github.com/KazaKago/76eac08fe048a8a3e4a9
補足
今回のクラスはJavaインタフェースによる標準的なコールバックを用いましたが、このほかにもEventBusなどを用いて通知を受け取るのも有効な手段です。