FragmentStatePagerAdapter で動的に Fragment を変更すると、Activity 復元時におかしくなる場合がある

FragmentStatePagerAdapter で動的にページを変更するといろいろとハマる。
Activity の復元時にも気をつける点があったので整理してみた。

## だめなパターン

具体的には FragmentStatePagerAdapter を使った Activity が OS に破棄され、再生成されて復帰時にページ(Fragment)のリストが変わっている場合。FragmentStatePagerAdapter は自前で各ページの状態を保持するため、復元後の Adapter に食い違いがあるとき、不整合が生じてしまうようだ。

PagerAdapter は以下のような感じで定義し、文字列 `fragmentId` で識別される `PageFragment` を各ページに表示してみる。

MyPagerAdapter

```kotlin
class MyPagerAdapter(
        fragmentManager: FragmentManager,
        private val fragmentIds: List<String>
) : FragmentStatePagerAdapter(fragmentManager) {
    override fun getCount(): Int = fragmentIds.count()
    override fun getItem(position: Int) =
        PageFragment.newInstance(fragmentIds[position])
    override fun getItemPosition(o: Any) = POSITION_NONE
}
```

Activity

```kotlin
private lateinit var adapter: MyPagerAdapter
private val fragmentIds = Collections.synchronizedList(mutableListOf<String>())

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    // fragmentIds を設定
    adapter = MyPagerAdapter(supportFragmentManager, fragmentIds)
}
```

例えば Activity 起動時に `fragmentIds` が `["test2"]` だった場合、`PageFragment#newIntance("test2")` で生成した Fragment がひとつ作成される。

Activity が一旦破棄されて復元されたときに `fragmentIds` が `["test1", "test2"]` に変わっていた場合、**1ページめの Fragment は作成済とみなされて**元の Fragment が復元され、2ページめは新たに `"test2"` で作成される。結果、`"test2"` で生成した Fragment が2つできてしまう。

Activity の破棄を挟まず `fragmentIds` の内容を変更して `MyPagerAdapter#notifyDataSetChanged()` した場合は、`MyPagerAdapter#getItemPosition()` で `POSITION_NONE` を返しているので一旦すべての Fragment が破棄されてから正しく生成し直されるため、問題は起こらない。

## 対策(案)

Activity が破棄されたときの Fragment と復元時に Adapter に与える `fragmentIds` に食い違いがないよう、Activity 再生成時の `onCreate()` で一旦破棄されたときの `fragmentIds`を復元してみる。

```kotlin
private lateinit var adapter: MyPagerAdapter
private val fragmentIds = Collections.synchronizedList(mutableListOf<String>())

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    if (savedInstanceState == null) {
        // fragmentIds 初期化
    } else {
        // Activity 再生成時は破棄時のものに一旦復元
        (savedInstanceState?.getSerializable("fragmentIds") as? Array<String>)?.let {
            fragmentIds.addAll(it)
        }
    }
    adapter = MyPagerAdapter(supportFragmentManager, fragmentIds)
}

override fun onSaveInstanceState(outState: Bundle?) {
    super.onSaveInstanceState(outState)
    outState?.putSerializable("fragmentIds", fragmentIds.toTypedArray())
}
```

問題は変更後の fragmentIds にどこで更新するかだが、ページャが実際に初期化されるのは `onResume()` で画面描画を開始したあと、View に対する `onMeasure()` の延長のようだ。それが完了する前に fragmentIds を書き換えてしまうと、同じことになる。しかし更新が完了したタイミングは Activity には通知されない(と思う)。

処理を更に追ってみると `onMeasure()` から `ViewPager` がページを更新する際、`PagerAdapter#startUpdate()` が呼ばれ、完了すると `PagerAdapter#finishUpdate()` が呼ばれているようだ。なので以下のように `finishUpdate()` を override して1回でもこのメソッドが呼ばれたかどうかを保持して、これが呼ばれた後に fragmentIds を更新するようにしてみたところ、とりあえずうまくいった。

```kotlin
class MyPagerAdapter(
        fragmentManager: FragmentManager,
        private val fragmentIds: List<String>
) : FragmentStatePagerAdapter(fragmentManager) {
    ...

    override fun finishUpdate(container: ViewGroup) {
        super.finishUpdate(container)
        // 初期化完了とみなす
    }
}
```

これで本当に問題ないかの確証はないので、正しい対処の方法があれば知りたいです・・・