Dialogはあまり深く考えずに使っても大丈夫だったのだが、DialogFragmentは落とし穴が多数あり、正しく使わないとアプリが落ちてしまう。具体的には、Fragmentの再生成が発生したときに正しく動作しなくなる。
以下では「FragmentからDialogFragmentを開き、結果(OK/Cancel)をFragmentに返す」という場合を例にして、DialogFragmentの落とし穴を回避するための書き方を説明する。
正しいコード
public class OkCancelDialog extends DialogFragment {
static public interface OkCancelListener {
void onDialogPositiveClick(long id);
void onDialogNegativeClick(long id);
}
// Fragmentの再生成の時に呼ばれるので、引数なしのpublicなコンストラクタが必要
public OkCancelDialog() {
}
// targetFragmentは、結果を受け取るFragment。OkCancelListenerを実装したFragmentであること。
public OkCancelDialog(long dataId, OkCancelListener targetFragment) {
if (targetFragment != null && !(targetFragment instanceof Fragment)) {
throw new RuntimeException("targetFragment is not Fragment");
}
// Fragmentの再生成後でも使いたい値は、bundleに入れてsetArgumentしておく。
Bundle bundle = new Bundle();
bundle.putLong("id", dataId);
setArguments(bundle);
// 結果を受け取るFragmentは、直接Fragmentの変数には入れずに、
// setTargetFragment()/getTargetFragment()を使う
setTargetFragment((Fragment)targetFragment, 0);
}
public long getDataId() {
return getArguments().getLong("id", -1);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setMessage("Message");
builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// getTargetFragment()は、Fragmentが再生成された場合でも、正しいinstanceを返してくれる。
OkCancelListener listener = (OkCancelListener) getTargetFragment();
listener.onDialogPositiveClick(getDataId());
dialog.cancel();
}
});
builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
OkCancelListener listener = (OkCancelListener) getTargetFragment();
listener.onDialogNegativeClick(getDataId());
dialog.cancel();
}
});
return builder.create();
}
}
間違い1 DialogFragmentをinner classにする
public class MyFragment extends Fragment {
public class OkCancelDialog extends DialogFragment {
...
}
}
Fragmentの再生成時には、自動的にFragmentのpublicな引数なしのコンストラクタが呼ばれる。inner classにするとFragmentの自動生成ができないため、落ちる。staticなinner classにするのでも良いが、出来ればファイルを分けるほうが(間違えにくいので)良い。
間違い2 引数なしのコンストラクタを作らない
public OkCancelDialog() {
}
を省略、またはprivateにすると、Fragmentの再生成時に落ちる。Fragmentの再生成時には、外から引数なしのコンストラクタが自動で呼ばれるためである。間違い3 結果を受け取るFragmentへの参照を直接保持する
private OkCancelListener listener = null;
public OkCancelDialog(long dataId, OkCancelListener targetFragment) {
listener = targetFragment;
...
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
...
builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
listener.onDialogPositiveClick(getDataId());
}
}
}
上のようにすると、Fragmentの再生成時にはメンバ変数listenerはnullになってしまう。なぜなら新しく作られたFragmentは以前とは別のオブジェクトで、かつ引数なしのコンストラクタが呼ばれているためである。しかも結果を受け取るFragmentも再生成されて別のオブジェクトになっているので、もはやlistenerの値は意味を持たない。setTargetFragment()/getTargetFragment()を使えば、Fragmentが再生成された場合でも、新しく生成されたFragmentの参照を取得することができる。
間違い4 setArguments()を使わずに、値をメンバ変数に覚えておく
private long dataId = -1;
public OkCancelDialog(long dataId, OkCancelListener targetFragment) {
this.dataId = dataId;
...
}
間違い3と似ているが、Fragmentが再生成されたときには、メンバ変数dataIdは-1になっている。再生成後でも使いたい値はBundleに入れ、setArguments()しておけばよい。
0 件のコメント:
コメントを投稿