Fragment使用時のIllegalStateException回避

前置き

このページの内容はstackoverflowの素晴らしい回答を参考にしている。

IllegalStateException?

Fragmentを使っていると、画面遷移の際に以下の様が例外が発生することがある。

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState

これは非同期処理などでFragmentの操作タイミングがonSaveInstanceState()よりも後になってしまった場合に発生するものだ。 onSaveInstanceState()ではFragmentの状態も含めた画面情報を保存するため、それ以降のタイミングでFragmentを操作するとおかしくなるぞ!ということだろう(多分)。 詳しいことは(解決策も)下記リンクに詳しくまとめられているので、じっくり読んで欲しい。 http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html

commitAllowingStateLoss?

FragmentTransaction.commit()する代わりにFragmentTransaction.commitAllowingStateLoss()することでこの例外は発生しなくなる。 これは文字通り「画面状態が失われてもいいからコミット」ということで、この時のコミット内容はonSaveInstanceState()で保存されなくなる。 Activityが再生成されたりすると、そのFragmentの状態が正しく復元されなくなるということだ。 これでは弊害も大きくなってしまうし、FragmentManager.popBackStack()のような処理にはそもそもAllowingStateLossなメソッドが存在しない。

じゃあどうするの?

この問題を根本的に回避するには、onSaveInstanceState()より後、savedInstanceStateから復元されるまでの間にFragmentを操作しなければ良い。 onSaveInstanceState()onPause()/onStop()の直後に発生するため、onPause()からonResume()の間に操作しなければ良い、ということになる。 つまり、onPause()より後のFragment操作リクエストはonResume()以降で反映されるようにタイミングをずらそうということになる。

実装

stackoverflowにスゴク綺麗なのが書いてあった ↑これ見てやれば間違いない

間違いないのだが、若干わかりにくいかな?と思ったのでサンプルプロジェクトをgithubに置いてみた。 https://github.com/nein37/FragmentHandlerDemo

stackoverflowの例では、Handlerを利用するActivity/Fragmentそれぞれによって実装し直す必要があったが、 サンプル内ではBaseHandlerFragmentがHandlerによってFragment操作タイミングをずらす機能を実装している。 これによって、Handlerの存在を意識せずにsendMessageprocessMessageの実装だけで良くなる。

このクラスを継承している場合、非同期処理からのFragment操作は以下のようになる。

Bundle bundle = new Bundle();
// 対象フラグメントに渡したいパラメータはこのタイミングで設定できる
bundle.putString(ToFragment.BUNDLE_TOAST_TEXT,"hogehogehogehoge");
// Fragment操作処理を登録
// このタイミングで直接Fragment操作を行うと落ちる場合がある
sendMessage(WHAT_REPLACE_FRAGMENT, bundle);

sendMessage()onPause()より前に呼ばれた場合、BaseHandlerFragment.processMessage()が即座に実行される。 onPause()より後に呼ばれた場合は次にonResume()が呼ばれた直後にこのメソッドが実行されることになる。 Message.whatによって対象操作の判別、Message.getDate()によってパラメータの受け渡しを行っている。

    @Override
    public void processMessage(Message message) {
        switch (message.what) {
            case WHAT_REPLACE_FRAGMENT:
                // bundleも取り出す
                ToFragment fragment = new ToFragment();
                fragment.setArguments(message.getData());
                FragmentTransaction ft = getFragmentManager().beginTransaction();
                ft.replace(getId(), fragment);
                ft.addToBackStack("hoge");
                ft.commit();
                break;
            default:
                break;
        }
    }

processMessage()の中にはFragmentTransaction.commit()に限らずFragmentManager.popBackStack()も書くことができるため、非同期処理から発生するFragment操作をすべてここに集約することでIllegalStateExceptionを回避することができる。 Fragment操作のタイミングをずらせれば良いのでこの通り実装する必要はないが、元記事HandlerMessageによるタイミングのずらし方が美しいのでぜひ参考にしてほしい。 そして、長々と書いたがそれを隠蔽しようとするこのサンプルはクソだ。

まとめ

Fragment最高。stackoverflowの実装は美しい。このページとサンプルはクソ。