2013年10月29日火曜日

[Android] Fragmentこれくしょん~ふらこれ~

※この記事は某オンラインゲームとは関係ありません

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.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編)を見てください)

他の方法としてはリスナーをSerializableにしたり、JSON(String)にするなど、Bundle型に格納できる型に変換するという方法があると思いますが、私はあまり詳しくないので、詳しく書いてあるEffective Android読むといいよ!

※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などのハマり所をまとめたいと思います。

参考サイト