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 件のコメント:
コメントを投稿