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(); } }
これはソースを見るとわかりますが、DialogFragmentがDialogにリスナー等をsetしていて、上書きされてしまうためです。
@Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); ... mDialog.setCancelable(mCancelable); mDialog.setOnCancelListener(this); mDialog.setOnDismissListener(this); ... }
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; } ... }; }
@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()で調べてみてください。
実は、上記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に適当な文字を入力した後に画面を再生成させると、入力していた文字が消えてしまいます。
ViewはActivityやFragmentと同様にonSaveInstanceState()で状態が保存されますが、Viewの場合はBundleではなくSparseArray<Parcelable>に保存されます。
SparseArrayのkeyとしてViewのidが使われるため、idがないと保存されずに再生成時に初期化されてしまいます。
SparseArrayのkeyとしてViewのidが使われるため、idがないと保存されずに再生成時に初期化されてしまいます。
XMLでレイアウトを書くときは(状態を保存する必要があるViewは)だいたいidを付けると思うので、この現象はレイアウトを動的に生成しないとまず発生しないと思います。
Viewを作る時はidを付ける癖をつけようねというお話しでした。
まとめ
DialogFragmentは前回のに加えてNo.005~No.007のポイントがあるので非常にハマりやすいです。
私はpublic static なインナークラスはあまり好きじゃないのでDialogなクラスまみれになって辛い(´・ω・`)
今まで紹介したもの以外に、タブを使ったり、FragmentPagerAdapterを使ったりするとまた違ったポイントがあったりするので、ネタがたまったらまた書きます。