Fragmentの設置について私がやってるベストプラクティス


先日、@kiriminさんからこんなツイートがありました。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">onCreateでのaddFragmentでFragment増殖するやつ三現場連続で遭遇しててBaseActivityにaddFragmentメソッドを生やすところまで同じなのでどこかに悪いサンプルがあるのではと疑ってる</p>&mdash; きりみん (@kirimin) <a href="https://twitter.com/kirimin/status/895107302167986177">2017年8月9日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">ピンとこなかったので記事で見てみたいです（無茶振り）</p>&mdash; なかざん@🍺 (@Nkzn) <a href="https://twitter.com/Nkzn/status/895111569041838080">2017年8月9日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

というわけで、雑に煽ってみたら記事を書いていただけました :raised_hands: 
ありがとうございました :pray::pray::pray::pray::pray: 

* [FragmentをonCreateで何も考えずにaddして増殖するケースが後を絶たない件](http://qiita.com/kirimin/items/59fcb626ee5a315a3684)

実はこの内容、4年くらい前からベストプラクティスとしてやっていました。特にアウトプットしたことがなかった（たぶん）ので、この機会に知っていることを吐き出しておこうと思います。

※この記事は上記記事への蛇足が中心なので、上記の記事を先にお読みください。

### TL;DR

* Activityの扱いを踏まえると、Fragmentの初期配置は `savedInstanceState == null` のときにやるほかないです
* savedInstanceStateで状態を保存しとくと何かと楽ですよ
* 筆者はaddよりreplaceのほうが好きです
* もっと知りたい人は[Master of Fragment](https://tatsu-zine.com/books/master-of-fragments)読んで

## 何が起こっているか

次の2文が核心を突いていたと思います。

> Activityがリストアされる度にAcrtivityのonCreateが呼ばれるため
> onSavedInstanceStateがnullじゃない場合にはaddを行わない

実は、ActivityはFragmentとsavedInstanceStateについて、かなり密接な扱いをしています。[SDK側のActivityの実装](https://github.com/android/platform_frameworks_base/blob/ab978c035eda5e6e42f3856d5edf132e76e45994/core/java/android/app/Activity.java)を見てみると、Fragmentを次のように扱っています。

```java:Activity.java
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する方法が、筆者にとってもベストプラクティスです。まさしくこれ↓ですね。

```java
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と仲良くしていきたい方は是非ご覧ください。

* [Master of Fragment (Android Professional Developerシリーズ) - 達人出版会](https://tatsu-zine.com/books/master-of-fragments)