Fragment を使った画面を作る際に、どのように作ればうまい具合に画面の回転や状態の復元を扱えるかという自分用のメモです。
最初にまとめ
- 基本方針として可能な限りすべての管理を当該ActivityのFragmentManagerに任せると楽
- ActivityのフィールドとしてFragmentを保持するのはバッドノウハウな気がする
- onCreateとonDestroyが呼び出されたからといってインスタンスが生成破棄されているとは限らない、これらはFragmentManagerのタイミング次第
最終的に実装したコードは以下のような感じになりました。
- Activity例: MainActivity.java
- Fragment例: NetworkMapFragment.java
今回の発端
ActionBarのタブに2つのFragmentを格納し、片方はListView, もう片方はGoogle MapsのMapViewを突っ込むようなUIを作っていたのですが、Androidは素人なもので普通に作っていると画面回転とタブの切替時にうまいこと状態を復元するのがなかなか手こずってしまいました。というわけで良いプラクティスを考えてみることにしました。
Activity と Fragmentのライフサイクルを復習
Activityは画面回転時に一度破棄されてしまいます。このような場合はActivityのFragmentManagerが現在管理しているFragment(バックスタックに入っているものが含まれるかどうかは未検証)については、Activity破壊時にFragmentManager経由で自動的にonSaveInstanceが呼び出され、Activity復旧時に自動的にonCreateとonCreateView経由で復元が試みられます。
これとは別に、Activityは破棄されないがFragmentは破棄されるケース、例えばActionBarのタブを切り替えたりNavigationDrawerを選択するなどして同一のActivity上で別の画面Fragmentに遷移する場合もあります。
上記いずれの場合も、可能な限りテキストビューの入力内容やリストのスクロール位置、地図のカメラ位置などを保持することをユーザーから期待されるため、状態の復元が必要になります。画面を回したりタブを切り替えたらスクロール位置が先頭に戻ったらユーザーはイライラするでしょう。
状態の保存はBundleとonSaveInstanceStateを使い、復元はonCreateとonCreateViewを使うのが楽です。
解決策
画面回転などActivity自体の破棄と再生成が自動的に行われるケースであればFragmentManager管理下にあるFragmentについて自動的に再生成が試みられるため大して難しくはないと思います。タブを切り替えたりするケースについては、以下のいずれかが良さそうな気がしています。
- 解決策1: すべてのFragmentをFragmentManagerにattachされた状態のままにし、タブが切り替えられたら見せないFragmentはhideする
- 解決策2: 必要に応じてFragmentManagerにattach/detachを行い、そのかわり自分でBundleを作りonSaveInstanceStateを呼び出す
1のメリットはタブ切り替え時に復元がそもそも発生しないため管理が簡単です。確実に動作しますし、再生成も必要ないためパフォーマンスも良いです。デメリットはFragmentおよびFragmentが抱えるView構造をすべて保持し続けるためメモリを大量に消費します。
今回採用した解決策2のメリットはタブ切り替え時にFragmentのView構造をすべて捨てるためメモリが効率的です。MapViewはどうしてもメモリを大量に使うためいくらhide状態とはいえあまり他のタブの後ろにおいておきたくはなかったのでこうしました。デメリットはやはり複雑になります。今回はFragmentのインスタンスフィールドとして一時的にBundleを保持していますが、これは正直なぜFragmentがタブから外れてDetachされてDestroyされてるのにメモリ上に残ってるのかわかりづらい変な挙動になるので、Activity側かまたは何らかのマネージャクラスに任せてしまうべきではないかと思います。・・・ってそれがFragmentManagerなんですけど。もっとうまいやり方で出来そうな気がするんですが・・・