FragmentStatePagerAdapter で動的に Fragment を変更すると、Activity 復元時におかしくなる場合がある
modified: 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)
// 初期化完了とみなす
}
}
これで本当に問題ないかの確証はないので、正しい対処の方法があれば知りたいです・・・