2011年11月にFragment(Android3.0)が誕生して早2年近く経ちましたが、皆様いかがお過ごしでしょうか。
Fragment使ってますか?使ってる人はこの文字列をよく見かけると思います。
IllegalStateException
みんな大好き() IllegalStateExceptionです。Fragmentは使い方を間違えるとよくこいつが発生します。
AlertDialog風なDialogFragmentを作成するでも言いましたが、DialogFragmentは特にハマり所が多いです。
私も1年以上Fragmentを使ってますがいまだにハマることが多々あります。なので、ハマるポイントをこれくしょん(まとめ)してみました。
No.001 画面再生成時にクラッシュする
これはFragmentをはじめてつかった時にやりがち。原因は複数あって、
- publicで引数なしのコンストラクタが無い
private HogeFragment() {
}
- staticじゃないインナークラス
public class HogeFragment extends DialogFragment {
}
- staticだけどpublicじゃないインナークラス
private static class HogeFragment extends DialogFragment {
}
RuntimeException(InstantiationException)が発生した時は、だいたいこの3つが原因です。No.002 画面再生成時に値が初期化される
だいたいFragment#setArguments()で値を保存していないのが原因です。Fragment#setArguments()についてはこちらをどうぞ。
ここではオレオレsetterダメよと書いていますが、一応こんな感じにすればオレオレsetterでも値は保持されます。
public class HogeFragment extends Fragment {
private static final String ARG_PARAM1 = "param1";
public void setParam1(String value) {
if (getArguments() == null) {
setArguments(new Bundle());
}
getArguments().putString(ARG_PARAM1, value);
}
}
ちなみに私は(Dialog)Fragmentをいつもこんな感じで書いてます。public class HogeFragment extends Fragment {
private static final String ARG_PARAM1 = "param1";
private static final String ARG_PARAM2 = "param2";
public static class Builder {
final Bundle mArgs;
public Builder() {
mArgs = new Bundle();
}
public Builder setParam1(String value) {
mArgs.putString(ARG_PARAM1, value);
return this;
}
public Builder setParam2(String value) {
mArgs.putString(ARG_PARAM2, value);
return this;
}
public HogeFragment create() {
HogeFragment f = new HogeFragment();
f.setArguments(mArgs);
return f;
}
}
}
BuilderかわいいよBuilder。
No.002 と同じ現象ですが、Fragment#setArguments()で保持できるBundle型は、そのままではリスナーなどを格納することができません。対処方法はいくつかあると思いますが、ここでは一番メジャーなActivityにリスナーを実装する方法を紹介します。
No.003 画面再生成時にリスナーが初期化される
No.002 と同じ現象ですが、Fragment#setArguments()で保持できるBundle型は、そのままではリスナーなどを格納することができません。対処方法はいくつかあると思いますが、ここでは一番メジャーなActivityにリスナーを実装する方法を紹介します。
public class HogeFragment extends Fragment {
private OnFragmentInteractionListener mListener;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Button button = new Button(getActivity());
button.setText(R.string.hello_world);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onButtonPressed(null);
}
});
return button;
}
// Activityにイベントを通知
public void onButtonPressed(Uri uri) {
if (mListener != null) {
mListener.onFragmentInteraction(uri);
}
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// Activityに実装されているリスナーを取得。実装されていない場合はエラー
try {
mListener = (OnFragmentInteractionListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement OnFragmentInteractionListener");
}
}
@Override
public void onDetach() {
super.onDetach();
mListener = null;
}
public interface OnFragmentInteractionListener {
public void onFragmentInteraction(Uri uri);
}
}
public class HogeActivity extends FragmentActivity implements HogeFragment.OnFragmentInteractionListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FragmentManager fm = getSupportFragmentManager();
fm.beginTransaction().replace(android.R.id.content, new HogeFragment()).commit();
}
@Override
public void onFragmentInteraction(Uri uri) {
// TODO Auto-generated method stub
}
}
これで画面再生成時にonAttach()が呼ばれるので、Activityからリスナーを再取得することができます。(色々な人が言ってますが私もこの書き方はあまり好きじゃないです...)
SDKの中にはテンプレートが付属されていますが、その一つにBlankFragmentという空(最小構成)のFragmentを作るテンプレートがあって、上記のような書き方になってるのでこれが現状ベターなのでしょう。(テンプレートの使い方はテンプレートを作成する(Object編)を見てください)
SDKの中にはテンプレートが付属されていますが、その一つにBlankFragmentという空(最小構成)のFragmentを作るテンプレートがあって、上記のような書き方になってるのでこれが現状ベターなのでしょう。(テンプレートの使い方はテンプレートを作成する(Object編)を見てください)
他の方法としてはリスナーをSerializableにしたり
※JSONは試してみましたがうまいこといかなかったです...
No.004 画面再生成時に値が初期化される その2
No.002 のFragment#setArguments()をやってるのに初期化される!...という方、No.003のHogeActivityのような書き方をしていませんか?サンプルなので最小構成で書いていますが、このままだと値は保持されないはずです。(正確には保持はされるが破棄される)
画面再生成時はFragmentだけでなく、Activityも再生成されるので、onCreate()などももう一度呼ばれます。No.003の例ではnew HogeFragment()で新しいインスタンスで置き換えているので初期化されてしまいます。
ではどうするかというと、onCreate()の引数であるsavedInstanceStateで判断します。savedInstanceStateは初回生成時はnullが、再生成時は保存された状態が格納されています。
public class HogeActivity extends FragmentActivity implements HogeFragment.OnFragmentInteractionListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
return;
}
FragmentManager fm = getSupportFragmentManager();
fm.beginTransaction().replace(android.R.id.content, new HogeFragment()).commit();
}
@Override
public void onFragmentInteraction(Uri uri) {
// TODO Auto-generated method stub
}
}
まとめ
紹介した4つのポイントですが、すべて再生成されなければ発生しません。毎回発生しないってのがまたハマるポイントですね。これを100%再現させるには、画面をバックグラウンドにした状態でアプリ(プロセス)を停止させるか、[設定]→[開発者向けオプション]→[アクティビティを保持しない]にチェックをつけることで可能です。
今回はFragmentを使い始めたときにハマるポイントを紹介しました。次回はDialogFragmentなどのハマり所をまとめたいと思います。
参考サイト