何故 DialogFragment か
Android 2.x 時代のダイアログの表示の仕方は以下のようなものだった:
public class MainActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showDialog(1); // Activity.showDialog() を使用する
}
});
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case 1:
return new AlertDialog.Builder(this)
.setTitle("てすと")
.setMessage("てすと")
.create();
default:
throw new IllegalArgumentException("unknown dialog id " + id + ".");
}
}
}
筆者も恥ずかしながら昔やっていたのだが、以下のように 直接 AlertDialog.Builder から create() して show() するのは多くの場合正しくない:
public class MainActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// AlertDialog のインスタンスが Activity のライフサイクルに乗らない!!
new AlertDialog.Builder(MainActivity.this)
.setTitle("てすと")
.setMessage("てすと")
.create().show();
}
});
}
}
これだと Activity のライフサイクルに乗らない為画面回転などで落ちる機種もある。落ちない機種であったとしても Logcat に WindowLeaked なるエラーが吐かれるので見てみよう。かなり多くのサイト、及び売られている書籍でも、ダイアログを直接表示しているサンプルコードが多いので注意。 落ちないようにするには自分で onDestroy()
でダイアログを閉じるように管理してやれば良いのだが、 それも厄介な話。
そしてこの Activity.showDialog()
だが Android 2.2 からは Bundle も渡せるようになった為動的なダイアログも作成できるようになった。というわけで便利に利用できるはずなのだが、この showDialog()
というか Activity の Dialog 関連のメソッドが軒並み deprecated (非推奨) になっている……。ドキュメントには DialogFragment を使ってください と書いてある。 Google 的には Fragment 関連に全て移行して欲しいようである。
※ただ Support Library を使った場合の 2.x での PreferenceActivity とか DialogFragment が使えない場面では仕方なく Activity.showDialog()
使う場合がある... もう特別な理由がない限り 4.0.3 over で開発したい
癖がある DialogFragment
DialogFragment は何となく使うと失敗する。まず筆者が最初にやってしまったダメなパターンは以下のようなものだ:
public class MainActivity extends FragmentActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// 無名クラスで手軽にこう表示したくなるが...
new DialogFragment() {
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle("てすと");
builder.setMessage("てすと2");
builder.setPositiveButton("OK", null);
builder.setNegativeButton("キャンセル", null);
return builder.create();
};
}.show(getSupportFragmentManager(), "dialog"); // 画面回転でインスタンス復元できない
}
}
上記の例は正しく表示されるが、画面回転時にエラーで落ちる。画面回転時にシステムによって状態を復元しにいくようだが、外部のクラス (この場合は MainActivity の外) から見えている状態で、 かつ外部から new できる状態でないといけない。なので、引数ありのコンストラクタを定義するなどして引数なしのコンストラクタを見えなくしてしまったりした場合も失敗する。
故に DialogFragment を使う初めのポイントとしては以下のようになる:
- DialogFragment の派生クラスは 外のクラスで public で定義するか public static な内部クラスとして定義 する
- DialogFragment の派生クラスで下手にコンストラクタを定義しない。 引数は必ず setArguments で渡す (コンストラクタ内でメンバ変数にリスナを設定するようなことはやってはいけない。arguments (Bundle) に入っていない情報は画面回転で消滅する)
Fragment をお使いの方はご存知と思うが上記は別に DialogFragment 固有でも何でもなく Fragment の制限と同一 である。いろいろな場面で面倒だと悩むことがあるかもしれないが、Fragment ど同じ制限を有する事を念頭に置いておけば方針を導きやすくなる。
setCancelable や OnCancelListener でハマる問題
DialogFragment#onCreateDialog()
内で AlertDialog.Builder に対していろいろスタイルを設定していくのが基本の使用法だが、 そうかと思って setCancelable()
でキャンセルできなくしようとしたり setOnCancelListener()
でキャンセル時にイベントを実行しようと思っても 効かない。
setCancelable()
は DialogFragment に対して実行し setOnCancelListener()
は DialogFragment#onCancel()
をオーバーライドする事によって実現できる。
ボタンを押したイベントを Activity や Fragment 側で受け取るには
1. Activity (TargetFragment) の参照をキャストして対象のメソッドを呼び出す
特定の Activity ないし Fragment に依存したコードを書く ことになるので汎用性は無いが後述の Interface を介すよりコードを書く量は減る。つまり以下のような感じである:
public Dialog onCreateDialog(Bundle savedInstanceState) {
// MyActivity のみで使える. 逆に言えば MyActivity に依存しているコード
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
...
((MyActivity) getActivity()).onDialogPositiveButtonClicked();
...
}
}
onCreateDialog()
のタイミングで ClassCastException が throw される可能性があるのは宜しくないので以下のように onAttach() でチェックした上で参照を持っておくのはよく使われるイディオムである:
public class MyDialogFragment extends DialogFragment {
/** Activity 参照. */
private MyActivity mActivity;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// MyActivity 以外に所属する場合 Exception をスローし先に進ませない
if (activity instanceof MyActivity == false) {
throw new UnsupportedOperationException("MyActivity 以外からコールされている.");
}
mActivity = (MyActivity) activity;
}
@Override
public void onDetach() {
super.onDetach();
mActivity = null; // Activity のリーク対策
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(mActivity)
.setTitle("てすと")
.setMessage("てすと")
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mActivity.doIt(); // MyActivity 固有のメソッドをコールして処理を戻す
}
})
.setNegativeButton("キャンセル", null) // リスナに null を入れると何もしない
.create();
}
}
特定の Fragment 上で DialogFragment を表示したい場合は TargetFragment (API 17 以上 or Support Library なら ParentFragment) を使用する。その場合もやはり以下のようにチェックすればよい:
/** Fragment 参照. */
private MyFragment mFragment;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// MyFragment 以外に所属する場合 Exception をスローし先に進ませない
final Fragment fragment = getTargetFragment();
if (fragment == null || fragment instanceof MyFragment == false) {
throw new UnsupportedOperationException("MyFragment を TargetFragment にセットせよ.");
}
mFragment = (MyFragment) fragment;
}
「この DialogFragment は絶対に特定の Activity (Fragment) 上でしか使わない」と断言できるのであればこの方法を採用してもよい、ということになるだろう。
2.Activity や Fragment に専用の Interface を実装する
どの Activity で使われるか分からない Fragment から Activity に処理を戻すときに使われるイディオムであり、これは DialogFragment でも有効である。「決まったタイトル、メッセージを出して終了するだけ」とか「プログレスを回すだけ」とか「通信エラー表示するだけ」みたいな定型的にどこでも使われそうなものはこれで定義すると良い:
public Dialog onCreateDialog(Bundle savedInstanceState) {
// 専用のインターフェースを実装している Activity ならどれでもよい.
// (やはり onAttach()あたりで getActivity() instanceof Callback とかで事前チェックすべき)
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
...
((Callback) getActivity()).onDialogPositiveButtonClicked();
...
}
}
public interface Callback {
void onDialogPositiveButtonClicked();
void onDialogNegativeButtonClicked();
}
番外: Bundle.setSerializable か setParcelable を使用してリスナをセットする
bundle で渡せば DialogFragment 復帰後も自動でデータ復帰してくれる。が Serializable と Parcelable の実装をそれぞれ行わなければならない。 やってみたが結局リスナに対して正しく Serializable や Parcelable の実装が出来なさそうにみえるので却下。
DialogFragment が面倒くさいと揶揄される理由
上記の通り使う度にクラスを作らなければならないように見えるのが辛い。あとこれは Android SDK の API のせいなのだが DialogFragment を show()
するコードももう少し短く書けないものかと思うだろう。これを何とかする為に、共通的に 1 つの DialogFragment 実装クラスで大抵の用途に対応できるようにするとか、皆様いろいろ試行錯誤されているように見える。
筆者が一つ辿り着いた DialogFragment の実装パターンがあるので記事を書いてみた。
DialogFragment 実装パターン を参照頂きたい。