ViewPager2 + FragmentStateAdapterでFragmentを追加・削除しようとしてハマった時のまとめ。
やりたいこと
- ViewPagerにセットしたFragmentを別のFragmentに置換したい

Android Developersに以下のような記載がありました。
ViewPager2 は、編集可能なフラグメント コレクションのページングをサポートしています。基盤コレクションが変更されたときに、notifyDatasetChanged() を呼び出して UI を更新します。
これにより、アプリは、実行時にフラグメント コレクションを動的に編集できるようになり、編集されたコレクションを ViewPager2 が正確に表示します。
そのため方針としては以下のように実装していきます。
- FragmentStateAdapterで表示するFragmentのリストを保持して、表示するFragmnetを操作する(add/remove)
- Fragmentのリストを操作したら、notify系のメソッドでUIを更新する
試したこと
FragmentStateAdapterを実装してみる
今回は以下の2パターンの置き換えを実装します。
- FragmentOne -> FragmentA
- FragmentTwo -> FragmentA, FragmentB
置き換える方法の判別はEnum classを利用して行います。
enum class ReplacePattern {
OneToA,
TwoToAB
}
class ViewPagerAdapter (fragment: Fragment): FragmentStateAdapter(fragment) {
private val fragmentList = mutableListOf<Fragment>()
/**
* 中略
**/
fun replaceFragment(pattern: ReplacePattern){
//FragmentOneをremoveして、FragmentAに置き換える
if (pattern == ReplacePattern.A) {
fragmentList.removeFirst()
fragmentList.add(0, (FragmentA()))
}
//FragmentTwoをremoveして、FragmentA, Bに置き換える
if (pattern == ReplacePattern.AB){
fragmentList.removeAt(1)
fragmentList.add(1, (FragmentA()))
fragmentList.add(2, (FragmentB()))
}
//notifyを呼んでUIを更新する
notifyDataset(pattern)
}
fun notifyDataSet(pattern: ReplacePattern){
//add,removeするFragmentに対応したpositionを指定
if (pattern == ReplacePattern.A){
notifyItemRemoved(0)
notifyItemInserted(0)
}
if (pattern == ReplacePattern.AB){
notifyItemRemoved(1)
notifyItemRangeInserted(1,2)
}
}
}
ただ、このコードでFragment操作を実行すると以下のエラーが出ます。
java.lang.IllegalStateException: Fragment already added
これはFragmentStateAdapterがコレクションをIDで管理しているためです。
そのためFragmentを操作する場合は、以下の2つのメソッドをoverrideする必要があります。
- getItemId(position: Int): Long
- getItemCount(position: Int): Long
FragmentをHashCodeで管理する
IDはLongで管理しているため、FragmentのHashCodeをIDとして利用することにします。
完成系が以下になります。
class ViewPagerAdapter (fragment: Fragment): FragmentStateAdapter(fragment) {
private val fragmentList = mutableListOf<Fragment>()
private val idsList = mutableListOf<Long>()
init {
fragmentList.apply {
add(FragmentOne())
add(FragmentTwo())
add(FragmentThree())
//FragmentをHashCodeで管理
forEach {
idsList.add(it.hashCode().toLong())
}
}
}
override fun getItemCount(): Int = fragmentList.size
override fun createFragment(position: Int): Fragment = fragmentList[position]
//以下の2つをoverrideしないと"java.lang.IllegalStateException"が発生する
override fun getItemId(position: Int): Long = idsList[position]
override fun containsItem(itemId: Long): Boolean = idsList.contains(itemId)
fun replaceFragment(pattern: ReplacePattern){
//FragmentOneをremoveして、FragmentAに置き換える
if (pattern == ReplacePattern.OneToA) {
fragmentList.removeFirst()
fragmentList.add(0, (FragmentA()))
}
//FragmentTwoをremoveして、FragmentA , Bに置き換える
if (pattern == ReplacePattern.TwoToAB){
fragmentList.removeAt(1)
fragmentList.add(1, (FragmentA()))
fragmentList.add(2, (FragmentB()))
}
//idを更新
idsList.clear()
fragmentList.forEach {
idsList.add(it.hashCode().toLong())
}
notifyDataSet(pattern)
}
private fun notifyDataSet(pattern: ReplacePattern){
if (pattern == ReplacePattern.OneToA){
notifyItemRemoved(0)
notifyItemInserted(0)
}
if (pattern == ReplacePattern.TwoToAB){
notifyItemRemoved(1)
notifyItemRangeInserted(1,2)
}
}
}
後はViewPagerAdapter#replaceFragment()をよしなに呼び出せば期待した動作が実行されます。
今回作成したサンプルアプリ↓
参考