本ドキュメントについて
Activity/Fragmentの再生成に関しての挙動と、画面回転の挙動をまとめ、それらを統一的に処理するためのクラスを提示する。
画面回転などのconfigChangeにどう対処するか
公式ドキュメントのHandling Runtime Changesでは、以下の二つの対処方法が紹介されている。
- configChanges属性を使い、Activity/Fragmentの再生成をさせない
- setRetainInstance(true)を使い、Fragmentの再生成をさせない。再生成前後で使いたいオブジェクトはそのFragmentに格納しておく。
これらの方法はいずれも、問題点がある。
ActivityのconfigChangesで再生成をさせない場合の問題点
Acdtivityの属性に
android:configChanges="orientation|keyboardHidden|screenSize"
のようにconfigChangesを指定してActivityの再生成をさせない(結果的にFragmentも再生成されない)という方法がある。
お手軽な方法ではあるが、画面サイズや向きに合わせたリソースの切り替えが行われない。
わかりやすい例を挙げると、ActionBarの文字のサイズは画面の向きによって自動で調整が行われる(landscape時の方がサイズが小さい)のだが、Activityの再生成が行われないと、文字サイズの調整が行われない。
setRetainInstance(true)の問題点
setRetainInstance(true)には、いくつか問題点がある。
-
backstackに入れることができない。(setRetainInstance()のドキュメントに、backstackに入れるなと書いてある)
-
loaderと一緒に使うとonLoadFinishedが呼ばれない。(以前ブログにまとめたので、そちらを参照)
この二つの制限があるため、setRetainInstance(true)はUIを持つFragmentで使うのが難しい。
では、公式ドキュメントのHandling Runtime Changesのように、UIを持たないFragment(Retained Fragment)に状態を保存すればいいのかというと、その場合でも問題がある。
公式ドキュメントの方法の問題点は、1つのActivityに対して、Retained Fragmentを一つしか持てないことである。
// find the retained fragment on activity restarts
FragmentManager fm = getFragmentManager();
dataFragment = (DataFragment) fm.findFragmentByTag(“data”);
// create the fragment and data the first time
if (dataFragment == null) {
// add the fragment
dataFragment = new DataFragment();
fm.beginTransaction().add(dataFragment, “data”).commit();
// load the data from the web
dataFragment.setData(loadMyData());
}
一つのFragment Managerでは、同じ名前を持つFragmentは一つしか持てない。その為、Retained Fragmentのインスタンスを一つしか持てない。
じゃあ別の名前を付ければ良いのでは?と思うかもしれないが、そこまでするのであればFragmentを使わない別の仕組みを用意した方が良い。その方法は後述する。
Activity/Fragmentの再生成が起こるパターン
ここで話を変えて、Activity/Fragmentの再生成に関しての挙動を説明する。
Activity/Fragmentの再生成には二つのパターンがある。
- processが死んだ後の再生成
- 回転などのconfigChangeでの再生成
1. processが死んだ後の再生成
processが死んだ場合でも、
onSaveInstanceState(Bundle outState)
でbundleにいれたデータは、onCreate(Bundle savedInstanceState)で渡ってくるので、それを元に状態を復帰させればよい。逆にいうと、Bundleに入れられないデータは破棄される。例えば、ThreadオブジェクトなどはBundleに入れられないので復帰できない。
2. 回転などのconfigChangeでの再生成
configChangeでの再生成の場合には、processが死ぬわけではないので、Threadオブジェクトなどはメモリからは無くならない。再生成前と後で同じThreadオブジェクトを使うにはどうすれば良いのか。
- そもそもconfigChangeの時に再生成させない(前述の、configChange属性を使う方法)
- 再生成前後のActivity/Fragmentでなんとかしてオブジェクトをやり取りする
configChange時に再生成させない方法は、前述の
android:configChanges="orientation|keyboardHidden|screenSize"
を使えばよい。
再生成前後でオブジェクトを渡す方法の一つとしてsetRetainInstance(true)があるが、前述のとおり問題点がある。
再生成前後でオブジェクトをやり取りする
Activity/Fragmentの再生成前後でオブジェクトをやり取りするには、Activity/Fragmentのライフサイクルに依存しない仕組みを用意する必要がある。要は、staticな変数に入れておいてやればよい。
staticな変数にオブジェクトを入れる場合、以下の点を考慮しなくてはならない。
- Activity/Fragmentが複数ある時に、どうやってその複数を区別して正しい相手に正しいデータを渡すか。
- Activity/Fragmentの再生成がもう起こらない場合(Backボタンを押した場合など)、そのオフジェクトをどうやって破棄するか。
- できればonSaveInstanceStateで保存するbundleと統一的に扱いたい。
Retain.java
前述の3つの問題を解決するクラスとして、Retain.javaを作った。使い方は以下の通り。
まず、保持しておきたいデータのクラスを作る
class SomeData implements Serializable {
String value;
Boolean on;
transient Thread thread;
}
この際、Bundleで保存されてほしいデータはtransientをつけず、Bundleで保存されて欲しくないデータ、またはBundleに保存できないデータはtransientを付ける。データのクラス自体はSerializableにする必要がある。
次に、Retainの変数をRetain.forSerializable()で作成し、retain.onCreate()でオブジェクトを復帰させる。
private Retain<SomeData> retain;
private SomeData someData;
@Override
protected void onCreate(Bundle savedInstanceState) {
retain = Retain.forSerializable()
someData = retain.onCreate(savedInstanceState);
if (someData == null) {
someData = new SomeData()
}
}
retain.onCreate()の戻り値がnullの場合は、復帰させるオフジェクトが無かった(=初めてActivity/Fragmentを起動した)場合なので、自分でnewしてやる。
復帰させるオフジェクトがあった場合にはそのオブジェクトが返る。processが生きていた場合には、transientな変数も残っている。processが死んでいた場合には、transientな変数は初期値に戻っている。
後は、onSaveInstanceState()とonDestroy()に以下の処理を書いてやる。
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
retain.onSaveInstanceState(outState, someData);
}
@Override
protected void onDestroy() {
super.onDestroy();
retain.onDestroy(this);
}
Retain.javaは前述の3つの問題をどう解決しているのか
1. 複数Activity/Fragmentをどう区別するか
Retainのインスタンスには一意なIDが割り振ってある。そのIDをonSaveInstanceStateでBundleに保存し、onCreateでIDをキーにオフジェクトを探す。こうすることで、Acvitity/Fragmentが複数ある場合でも、区別することができる。
オフジェクトをいつ破棄するか
retain.onDestroy()で、ActivityのisFinishing()、FragmentのisRemoving()を呼び、戻り値がtrueの場合にはオフジェクトがもう使われることが無いので、破棄している。
bundleと統一的に扱いたい
オフジェクト自体をSerializableにするという制限をいれ、このオブジェクトを直接bundleにputSerializable()で保存している。オブジェクトをSerializableにしなくてはいけないという制限は生まれるが、bundleと統一的に扱えるメリットが大きいと判断した。