先日、@kiriminさんからこんなツイートがありました。
onCreateでのaddFragmentでFragment増殖するやつ三現場連続で遭遇しててBaseActivityにaddFragmentメソッドを生やすところまで同じなのでどこかに悪いサンプルがあるのではと疑ってる
— きりみん (@kirimin) 2017年8月9日
ピンとこなかったので記事で見てみたいです(無茶振り)
— なかざん@🍺 (@Nkzn) 2017年8月9日
というわけで、雑に煽ってみたら記事を書いていただけました
ありがとうございました
実はこの内容、4年くらい前からベストプラクティスとしてやっていました。特にアウトプットしたことがなかった(たぶん)ので、この機会に知っていることを吐き出しておこうと思います。
※この記事は上記記事への蛇足が中心なので、上記の記事を先にお読みください。
TL;DR
- Activityの扱いを踏まえると、Fragmentの初期配置は
savedInstanceState == null
のときにやるほかないです - savedInstanceStateで状態を保存しとくと何かと楽ですよ
- 筆者はaddよりreplaceのほうが好きです
- もっと知りたい人はMaster of Fragment読んで
何が起こっているか
次の2文が核心を突いていたと思います。
Activityがリストアされる度にAcrtivityのonCreateが呼ばれるため
onSavedInstanceStateがnullじゃない場合にはaddを行わない
実は、ActivityはFragmentとsavedInstanceStateについて、かなり密接な扱いをしています。SDK側のActivityの実装を見てみると、Fragmentを次のように扱っています。
protected void onCreate(@Nullable Bundle savedInstanceState) {
/* 略 */
if (savedInstanceState != null) {
Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
mFragments.restoreAllState(p, mLastNonConfigurationInstances != null
? mLastNonConfigurationInstances.fragments : null);
}
/* 略 */
}
protected void onSaveInstanceState(Bundle outState) {
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
Parcelable p = mFragments.saveAllState();
if (p != null) {
outState.putParcelable(FRAGMENTS_TAG, p);
}
getApplication().dispatchActivitySaveInstanceState(this, outState);
}
メモリ枯渇("Activityを保持しない"で再現できるのはこれ)や画面回転などによりActivityが破棄されるときに呼ばれる onSaveInstanceState
の中で FRAGMENTS_TAG
という名前でFragmentが保存されています。それが、 savedInstanceState != null
のとき、つまり再生成後のonCreateで復元されています。
そう、 ActivityはsavedInstanceStateでViewの状態と同じようにFragmentそのものも復元するのです。super.onCreate
を実行した時点で、破棄される前に配置されていたFragmentがそのまま再配置されているわけなので、そこに新たにFragmentをaddすれば、もちろん積まれることになります。
きりみんさんの記事でreplaceを使ったパターンのときにFragmentが2回生成されているのも、Activityのsuper.onCreate
で再生成された分と、その後でreplaceにより生成された分だと思われます。
どうすればよいか
やはり savedInstanceState == null
をチェックして、初回のonCreateのときだけFragmentをaddする方法が、筆者にとってもベストプラクティスです。まさしくこれ↓ですね。
if (savedInstanceState == null) {
getSupportFragmentManager()
.beginTransaction()
.add(R.id.container, new MainFragment())
.commit();
}
少し観点を変えると、Fragmentの扱いをActivityのsavedInstanceStateの仕組みに委ねるということでもあります。Fragmentの中で各種の状態をしっかりとsavedInstanceStateの管理下に置いていれば、それらも復元されるということです。
上手く使えば画面回転などの際に考えることが減るので、積極的に使っていきたいところです。
おまけ: addとreplaceの使い分け
実は筆者はFragmentの配置にaddを使うことがほとんどありません。バックスタックを有効に使おうとしているときでない限りは、現在どのFragmentが積まれているのかという状態を考えたくないのです。
一方、replaceは気楽です。ひとつのレイアウトにひとつのFragmentしかattachされていないことを保証できます。
そんなわけで、筆者は基本的にreplaceを使うことにしており、余程バックスタックを使いたくなったときだけaddを使っています。経験上、そのほうがトラブルが少ないと感じています。
まとめ
というわけできりみんさんの記事に蛇足を付け足してみました。Fragmentは普通に使うだけでもこういう罠が随所に隠れているので、注意が必要ですね。
Fragmentに関する知見はあんざいゆきさんのMaster of Fragmentにとてもよくまとまっているので、Fragmentと仲良くしていきたい方は是非ご覧ください。