google-code-prettify

2012-11-01

DialogFragmentの落とし穴にはまらないための方法

AndroidにFragmentが導入されて以来、Dialogを直接使うことは非推奨となり、DialogFragmentを使うことが推奨されている。

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