【Android】ViewPager2+FragmentStateAdapterでFragmentを置き換える

ViewPager2 + FragmentStateAdapterでFragmentを追加・削除しようとしてハマった時のまとめ。


##やりたいこと

* ViewPagerにセットしたFragmentを別のFragmentに置換したい

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704986/37f083c2-a737-6418-eaa4-368c7e98faaa.gif" width="300">
##方針

Android Developersに以下のような記載がありました。

>ViewPager2 は、編集可能なフラグメント コレクションのページングをサポートしています。基盤コレクションが変更されたときに、notifyDatasetChanged() を呼び出して UI を更新します。
これにより、アプリは、実行時にフラグメント コレクションを動的に編集できるようになり、編集されたコレクションを ViewPager2 が正確に表示します。

https://developer.android.com/training/animation/vp2-migration

そのため方針としては以下のように実装していきます。

* FragmentStateAdapterで表示するFragmentのリストを保持して、表示するFragmnetを操作する(add/remove)
* Fragmentのリストを操作したら、notify系のメソッドでUIを更新する


##試したこと
####FragmentStateAdapterを実装してみる

今回は以下の2パターンの置き換えを実装します。

* FragmentOne -> FragmentA
* FragmentTwo -> FragmentA, FragmentB

置き換える方法の判別はEnum classを利用して行います。

```ReplacePattern.kt
enum class ReplacePattern {
    OneToA,
    TwoToAB
}
```


```ViewPagerAdapter.kt
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として利用することにします。
完成系が以下になります。


```ViewPagerAdapter.kt
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()をよしなに呼び出せば期待した動作が実行されます。

今回作成したサンプルアプリ↓

https://github.com/taratara10/ViewPagerSample

###参考



https://dharmmobiledev.medium.com/add-remove-and-refresh-fragments-in-viewpager2-c2d61b56f5fd