LoginSignup
7
4

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-02-18

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

だめなパターン

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

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

MyPagerAdapter

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

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を復元してみる。

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 を更新するようにしてみたところ、とりあえずうまくいった。

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

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

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

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4