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()をよしなに呼び出せば期待した動作が実行されます。
今回作成したサンプルアプリ↓
参考