2013年11月8日金曜日

[Android] Fragmentこれくしょん・改

前回Fragmentを初めて使うときによくハマるポイントを紹介しました。今回はDialogFragment周りのポイントをこれくしょんします。

No.005 setCancelableが仕事してくれない


ずっとshowDialog()を使ってきた人はまずやらかすはずです。
このように書くとsetCancelable()にfalseを指定してもキャンセルできてしまいます。
public class HogeDialogFragment extends DialogFragment {

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        return new AlertDialog.Builder(getActivity())
                .setMessage(R.string.hello_dialog_fragment)
                .setCancelable(false)
                .create();
    }

}

同様にOnCancelListenerOnDismissListenerDialogに直接setすると期待通りに動いてくれません。
これはソースを見るとわかりますが、DialogFragmentDialogにリスナー等をsetしていて、上書きされてしまうためです。
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        ...

        mDialog.setCancelable(mCancelable);
        mDialog.setOnCancelListener(this);
        mDialog.setOnDismissListener(this);

        ...

    }

これを解決するには、DialogFragmentにあるsetCancelable()onCancel()onDismiss()を使います。
public class HogeDialogFragment extends DialogFragment {

    public static HogeDialogFragment newInstance(boolean cancelable) {
        HogeDialogFragment f = new HogeDialogFragment();
        f.setCancelable(cancelable);
        return f;
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        return new AlertDialog.Builder(getActivity())
                .setMessage(R.string.hello_dialog_fragment)
                .create();
    }
    
    @Override
    public void onCancel(DialogInterface dialog) {
        // TODO Dialogをキャンセルした時の処理
        super.onCancel(dialog);
    }

    @Override
    public void onDismiss(DialogInterface dialog) {
        // TODO Dialogを閉じた時の処理
        super.onDismiss(dialog);
    }

}
ここで注意点ですが、onDismiss()を使用する場合はsuper.onDismiss()を呼ぶのを忘れないでください。super.onCancel()では処理をしていないので無くても大丈夫ですが、super.onDismiss()ではDialogやFragmentの操作をしているので、無いと動作がおかしくなります。

No.006 DialogFragmentの多重起動


例えばボタンを押すとダイアログを表示するFragmentがあるとします。
public class HogeFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        Button b = new Button(getActivity());
        b.setText("Show");
        b.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                HogeDialogFragment.newInstance().show(getFragmentManager(), "dialog");
            }
        });
        return b;
    }

}
このようにすると、ボタンを押すたびにダイアログが表示されます。「ダイアログ表示してたらボタン押せないから多重起動しないだろ!」...と思うかもしれませんが、ダイアログが表示される前にボタンを連打すると発生するはずです。

いつもボタンを連打する人はまずいないですが、誤って2回タップしてしまうことは日常でありえると思うので、防いでおいた方がいいでしょう。

多重起動を防ぐにはfindFragmentByTag()を使います。
public class HogeFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        Button b = new Button(getActivity());
        b.setText("Show");
        b.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (getFragmentManager().findFragmentByTag("dialog") == null) {
                    HogeDialogFragment.newInstance().show(getFragmentManager(), "dialog");
                }
            }
        });
        return b;
    }

}
今表示してるダイアログを閉じてから表示するときはこんな感じにします。
public class HogeFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        Button b = new Button(getActivity());
        b.setText("Show");
        b.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                FragmentTransaction ft = getFragmentManager().beginTransaction();
                Fragment f = getFragmentManager().findFragmentByTag("dialog");
                if (f != null) {
                    ft.remove(f);
                }
                HogeDialogFragment.newInstance().show(ft, "dialog");
            }
        });
        return b;
    }

}

No.007 DialogFragmentのshow/dismissでクラッシュ


おそらく発生するエラーはIllegalStateExceptionだと思います。
これが発生する原因は2つあります(もっとあるかも)。

  • "Can not perform this action after onSaveInstanceState"
onSaveInstanceState()が実行された後(onStop()とかonDestroy()とか)にFragmentを操作しようとすると発生します。onSaveInstanceState()の前にshow/dismissをするようにしてください。

  • "Can not perform this action inside of  xxx"
xxxの中ではshow/dismissはできないよというエラーです(発生理由はさっきと同じで状態が保存された後に呼びだされるため)。LoaderManager.LoaderCallbacks#onLoaderFinished()LoaderManager.LoaderCallbacks#onLoaderReset()などで発生します。対処としては、
loadInBackground()で操作する、
    @Override
    public Loader<Object> onCreateLoader(int id, Bundle args) {
        return new AsyncTaskLoader<Object>(getActivity()) {
            @Override
            public Object loadInBackground() {

                // 時間のかかる処理

                HogeDialogFragment f = (HogeDialogFragment) getFragmentManager().findFragmentByTag(TAG);
                f.dismiss();

                return result;
            }

            ...

        };
    }

Handlerにpostして操作する、
    @Override
    public void onLoadFinished(Loader<Object> loader, Object data) {
        new Handler().post(new Runnable() {
            @Override
            public void run() {
                HogeDialogFragment f = (HogeDialogFragment) getFragmentManager().findFragmentByTag(TAG);
                f.dismiss();
            }
        });
    }
などの方法があります。

実は、上記2つのタイミングでもFragmentを操作する方法があります(DialogFragmentは閉じる場合限定(API Level19現在))。理解して使わないとかえってハマる可能性があるので(私はあまり使いません)詳しく紹介しませんが、興味がある人はcommitAllowingStateLoss()dismissAllowingStateLoss()で調べてみてください。

No.008 Viewの値が保持されない


これは前回紹介した再生成されないと発生しないパターンなのですが、もはやFragmentと直接は関係ないのでここで紹介します。

例えばEditTextを表示するDialogFragmentがあるとします。
public class HogeDialogFragment extends DialogFragment {

    private EditText mEditText;

    public static HogeDialogFragment newInstance() {
        HogeDialogFragment f = new HogeDialogFragment();
        return f;
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        mEditText = new EditText(getActivity());
        return new AlertDialog.Builder(getActivity())
                .setTitle(R.string.app_name)
                .setView(mEditText)
                .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                    }
                })
                .setNeutralButton(android.R.string.cancel, null)
                .create();
    }

    public String getText() {
        return mEditText.getText().toString();
    }

}
このDialogFragmentを表示させて、EditTextに適当な文字を入力した後に画面を再生成させると、入力していた文字が消えてしまいます。

ViewActivityFragmentと同様にonSaveInstanceState()で状態が保存されますが、Viewの場合はBundleではなくSparseArray<Parcelable>に保存されます。
SparseArrayのkeyとしてViewのidが使われるため、idがないと保存されずに再生成時に初期化されてしまいます。

XMLでレイアウトを書くときは(状態を保存する必要があるViewは)だいたいidを付けると思うので、この現象はレイアウトを動的に生成しないとまず発生しないと思います。

Viewを作る時はidを付ける癖をつけようねというお話しでした。

まとめ


DialogFragment前回のに加えてNo.005~No.007のポイントがあるので非常にハマりやすいです。
私はpublic static なインナークラスはあまり好きじゃないのでDialogなクラスまみれになって辛い(´・ω・`)

今まで紹介したもの以外に、タブを使ったり、FragmentPagerAdapterを使ったりするとまた違ったポイントがあったりするので、ネタがたまったらまた書きます。