TabbedActivityのサンプルプロジェクトをViewPager2化する

TabLayout+ViewPagerな画面を作る場合、新規ActivityのGaralyにあるTabbedActivityが参考になります。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/77f16ab6-4aa9-b4ae-9949-8070b4c0f3f9.png)
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/153d4466-98c9-4699-096b-55889bcfb19a.png)
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/2fbde9b6-e8ba-7b09-7a99-a630e6112025.png)

ただ、Android Studio 4.1.3では、タブのフォアグラウンドとバックグラウンドが同じ色になっていて、テキストもインジケータも見えないとかちょっとおかしなことになっています。
タイトル部分もToolbarと思いきやTextViewですね。どうしてでしょ？

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/ce8726f1-9323-498b-3026-f5da9861bce0.png" width="400"/>

ViewPager2にする話をしたいのですが、その前にちょっと手直ししておきます。

# ViewPagerサンプルとしての手直し

レイアウトはTextViewをToolbarに置き換えておきます（FloatingActionButtonは本題から外れるので外しています）

```xml:activity_view_pager.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".a.ViewPagerActivity"
    >

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/Theme.MyApplication.AppBarOverlay"
        >

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/Theme.MyApplication.PopupOverlay"
            />

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/tabs"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            />
    </com.google.android.material.appbar.AppBarLayout>

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
```

配色の問題についてはこれだけで解決します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/3a030c70-3618-d20b-234c-c78090845a47.png" width="400"/>

ActivityについてはViewBindingを使うように修正していますがほぼ変更はありません。

```kotlin:ViewPagerActivity.kt
class ViewPagerActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityViewPagerBinding.inflate(layoutInflater)
        setContentView(binding.root)
        setSupportActionBar(binding.toolbar)
        val sectionsPagerAdapter = SectionsPagerAdapter(this, supportFragmentManager)
        binding.viewPager.adapter = sectionsPagerAdapter
        binding.tabs.setupWithViewPager(binding.viewPager)
    }
}
```

ViewModelについてはそのままにしています。

```kotlin:PageViewModel.kt
class PageViewModel : ViewModel() {
    private val _index = MutableLiveData<Int>()
    val text: LiveData<String> = Transformations.map(_index) { "Hello world from section: $it" }

    fun setIndex(index: Int) {
        _index.value = index
    }
}
```

PagerAdapterについては結構大きく変えました。

FragmentPagerAdapterはFragmentManagerだけのコンストラクタはDeprecatedのため、新しいbehaviorを指定するコンストラクタを使い、BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENTを指定します。behaviorを省略した場合BEHAVIOR_SET_USER_VISIBLE_HINTを指定したのと同じ挙動になりますが、こちらの動作自体がDeprecatedです。
BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENTを指定すると、名前の通り、ユーザーに見えているFragmentだけがResume状態となるように動作します。旧来の動作では、表示されていないFragmentもResume状態となり、ユーザに見えているかどうかは`setUserVisibleHint`で検出していました。この動作はDeprecatedなので今後は使わないようにしましょう。
なお、ViewPager2の動作もBEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENTと同様です。

コンストラクタではViewModelの初期設定を行っています。サンプルではなぜかPageViewModelをFragmentスコープで持ち、Fragmentで初期化するということをやっています。これだと、FragmentがリサイクルされたときにViewModelも失われるので表示状態を保持することができません。ここにあるようにActivityスコープでページのポジションをキーとして保持するようにする必要があります。


```kotlin:SectionsPagerAdapter.kt
private val TAB_TITLES = arrayOf(
    R.string.tab_text_1,
    R.string.tab_text_2
)

class SectionsPagerAdapter(
    private val activity: FragmentActivity, fm: FragmentManager
) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
    init {
        repeat(count) {
            activity.pageViewModels<PageViewModel>({ it }).value.setIndex(it + 1)
        }
    }

    override fun getItem(position: Int): Fragment =
        PlaceholderFragment.newInstance(position)

    override fun getPageTitle(position: Int): CharSequence =
        activity.resources.getString(TAB_TITLES[position])

    override fun getCount(): Int = 2
}
```

pageViewModelsはこういう定義になっています。`keyedViewModels`については「[keyを指定したViewModelの取得もlazy拡張関数で簡単にしたい](https://qiita.com/ryo_mm2d/items/ceca85d85874e51cd53c)」をご参考

```kotlin:ViewModelExtensions.kt
@MainThread
inline fun <reified VM : ViewModel> ComponentActivity.pageViewModels(
    noinline pageIndex: () -> Int,
    noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null
): Lazy<VM> = keyedViewModels({ pageIndex().toString() }, factoryProducer)

@MainThread
inline fun <reified VM : ViewModel> Fragment.pageViewModels(
    noinline pageIndex: () -> Int,
    noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null
): Lazy<VM> = keyedActivityViewModels({ pageIndex().toString() }, factoryProducer)
```

最後にFragmentです。コンストラクタでレイアウトを指定し、onCreateやonCreateViewはoverrideしません。
代わりにonViewCreatedでラベル設定を行います。
サンプルのままでは、FragmentでLiveDataをobserveするときはlifecycleOwnerはviewLifecycleOwnerを使えと怒られるので怒られないように修正。

```kotlin:PlaceholderFragment.kt
class PlaceholderFragment : Fragment(R.layout.fragment_view_pager) {
    private val pageViewModel: PageViewModel by pageViewModels({
        requireArguments().getInt(KEY_POSITION)
    })

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val binding = FragmentViewPagerBinding.bind(view)
        pageViewModel.text.observe(viewLifecycleOwner) {
            binding.sectionLabel.text = it
        }
    }

    companion object {
        private const val KEY_POSITION = "ARG_POSITION"

        @JvmStatic
        fun newInstance(sectionNumber: Int): PlaceholderFragment =
            PlaceholderFragment().apply {
                arguments = bundleOf(KEY_POSITION to sectionNumber)
            }
    }
}
```

# ViewPager2化する

ここから、前項でViewPagerで実装していた部分をViewPager2に置き換えていきます。
同じ構成にするならViewModel/Fragmentは変更する必要はありません。

PagerAdapterはFragmentPagerAdapterもしくはFragmentStatePagerAdapterを使っているところを、FragmentStateAdapterに置き換えます。

getItemの代わりにcreateFragment、getCountの代わりにgetItemCountを実装します。
TabLayoutにタイトルを伝えるgetPageTitleに相当するメソッドはありません。この処理はActivityの方に移動させています。

```kotlin:SectionsPagerAdapter.kt
class SectionsPagerAdapter(
    activity: FragmentActivity
) : FragmentStateAdapter(activity) {
    init {
        repeat(itemCount) {
            activity.pageViewModels<PageViewModel>({ it }).value.setIndex(it + 1)
        }
    }

    override fun getItemCount(): Int = 2

    override fun createFragment(position: Int): Fragment =
        PlaceholderFragment.newInstance(position)
}
```

レイアウトでは、ViewPagerのところをViewPager2に置き換えます

```activity_view_pager.xml
-     <androidx.viewpager.widget.ViewPager
+     <androidx.viewpager2.widget.ViewPager2
```

AcitivtyではTabLayoutとViewPagerの接続部分を変更します。
`setupWithViewPager`は使えないので、TabLayoutMediatorを使って実装します。
こちらはtabへの反映処理を実装できるので、単にテキストを変更する以上の操作も可能ですね。

```kotlin:ViewPagerActivity.kt
private val TAB_TITLES = arrayOf(
    R.string.tab_text_1,
    R.string.tab_text_2
)

class ViewPagerActivity : AppCompatActivity() {
    private lateinit var binding: ActivityViewPager2Binding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityViewPager2Binding.inflate(layoutInflater)
        setContentView(binding.root)
        setSupportActionBar(binding.toolbar)
        val sectionsPagerAdapter = SectionsPagerAdapter(this)
        binding.viewPager.adapter = sectionsPagerAdapter
        TabLayoutMediator(binding.tabs, binding.viewPager) { tab, position ->
            tab.text = getString(TAB_TITLES[position])
        }.attach()
    }
}
```

以上が、ViewPager2に対応したTabbedActivityのサンプルになります。