ViewPager/ViewPager2を利用したfragmentのスワイプによる遷移とライフサイクル

横スワイプによる画面遷移アプリを作成するために、viewPagerを利用していたのですが、ライフサイクルに関する思わぬ沼にはまり苦労しました。
そこで、viewPagerとviewPager2をそれぞれ利用してfragmentをスワイプで遷移する場合のfragmentのライフサイクルについてまとめました。

##はじめに
現在では**viewPagerは非推奨となっており、viewPager2を使うように**とのこと。viewPagerでのいくつかの問題が修正されているほか、アクティブな開発サポートを受けられることや、垂直方向のスワイプ遷移等ができるそう。

スワイプ機能のあるアプリを作ろうと検索するとviewPagerを利用したものばかりが引っ掛かったのでその点は注意。

詳しくはdeveloperのページを参照してください。

https://developer.android.com/training/animation/screen-slide-2?hl=ja

https://developer.android.com/training/animation/vp2-migration?hl=ja


##fragmentのライフサイクル
![fragment_lifecycle.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/668653/a540ee9e-9af6-6f40-7dfb-215dbf336069.png)
画像：　[Androidデベロッパー　フラグメントより](https://developer.android.com/guide/components/fragments?hl=ja)

以下に主なコールバックメソッドを簡単に説明します。

より詳しい内容については以下のサイトをご覧ください。

https://www.atmarkit.co.jp/ait/articles/1604/04/news011_3.html

###onCreate()
フラグメントが生成されるときに呼び出される。
###onCreateView()
画面を描画、viewを生成する。

###onViewCreated()
viewが生成されたら呼び出される。viewの初期化やフラグメントの状態の復元はここで行うほうがよい。

###onReseme()
まさにスマホの画面に表示されたとき、つまりユーザーが操作を行えるようになる直前にに呼び出される。

###onPause()
画面に表示されなくなる、ユーザーが操作できなくなる時に呼び出される。

###onDestroy()
フラグメントが破棄される


##スワイプでfragmentを遷移するコード

フラグメントのライフサイクルを調べるために、まずはviewPagerを利用してスワイプで画面遷移ができるアプリを作成します。

![fragment.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/668653/a0f2221b-a3a7-5d06-76f9-d338e763ce69.jpeg)

上の図はアプリのアクティビティとフラグメントの構成を示したものです。
各画面を表示させるのはフラグメントで、どのフラグメントを表示させるか制御するのがアダプター、そしてそのアダプターはMainActivity上に生成されます。

###フラグメントの生成

最初に画面となるフラグメントを生成。
File > New > Fragment > Fragment(Blank)を選択し、名前を入力。するとFragmentクラスとそのレイアウトファイルが生成されます。

Fragmentファイルにはいろいろ書かれていますが、onCreateView以外の部分は消して問題ないです。（画面生成をするonCreateViewは必須）

```kotlin:Fragment1.kt
package your_package_name

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

class Fragment1 : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_1, container, false)
    }
}
```

レイアウトファイルは「fragment1」と左上に表示するだけのものです。

```xml:fragment_1.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".Fragment1">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="fragment1"
        android:textSize="36sp"/>

</FrameLayout>
```
同様に2~4のフラグメントも作成します。

###アダプターの生成
FragmentStatePagerAdapterアダプタークラスを生成します。
getItemとgetCountは必須で、それぞれ表示するフラグメントの制御と、コンテンツのサイズ（数）をセット。

```kotlin:SamplePagerAdapter.kt
package your_package_name

import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter

class SamplePagerAdapter(fm: FragmentManager, private val fragmentList: List<Fragment>) :
    FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {

    // 表示するフラグメントを制御する
    override fun getItem(position: Int): Fragment {
        return fragmentList[position]
    }

    // viewPagerにセットするコンテンツ(フラグメントリスト)のサイズ
    override fun getCount(): Int {
        return fragmentList.size
    }
}
```

###MainActivityの生成
```kotlin:MainActivity.kt
package your_package_name

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.viewpager.widget.ViewPager


class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        /// フラグメントのリストを作成
        val fragmentList = arrayListOf<Fragment>(
            Fragment1(),
            Fragment2(),
            Fragment3(),
            Fragment4()
        )

        /// adapterのインスタンス生成
        val adapter = SamplePagerAdapter(supportFragmentManager, fragmentList)
        /// adapterをセット
        val viewPager = findViewById<ViewPager>(R.id.viewPager)
        viewPager.adapter = adapter
    }
}
```
viewPagaerにセットするリストを生成します。
adapterのインスタンスをセットし、layoutで設置したViewPagerにadapterをセットします。

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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=".MainActivity">

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
```

できあがったアプリはこんな感じでスワイプでフラグメントを変えられます。
![238eb94fbbdfc65b916a5830a93410dd.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/668653/4e279cf2-16b4-704e-a152-3b8fcd7eabb6.gif)


##viewPagerを利用した場合のfragmentライフサイクル

アプリが出来たので実際にフラグメントのライフサイクルを見ていきたいと思います。
各フラグメントにon～のコールメソッドが呼び出されたときにSystemに出力させるようにして状態をチェックします。

```kotlin:Fragment1.kt
class Fragment1 : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        println("1 create")
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        println("createview")
        return inflater.inflate(R.layout.fragment_1, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        println("1 viewCreated")
    }

    override fun onStart() {
        super.onStart()
        println("1 start")
    }

    override fun onResume() {
        super.onResume()
        println("1 resume")
    }

    override fun onPause() {
        super.onPause()
        println("1 pause")
    }

    override fun onStop() {
        super.onStop()
        println("1 stop")
    }

    override fun onDestroyView() {
        super.onDestroyView()
        println("1 destroyview")
    }

    override fun onDestroy() {
        super.onDestroy()
        println("1 destroy")
    }

}
```
フラグメント1⇒フラグメント4へ順番にスワイプし、再度4⇒1に戻る場合を調べます。

黄色マス：表示画面
青マス：フラグメント生成
灰色マス：フラグメント破棄

![viewpager.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/668653/60d6a547-d423-e9bd-be94-f6fdfbfbcbee.jpeg)

上記から表示されているフラグメントの隣のフラグメントは生成されていることがわかります。
また画面を移ったときに、隣のフラグメントであればpuaseになりますが、隣以外のフラグメントは破棄されていることがわかります。

今回困ったことはこのフラグメントを破棄⇒生成を繰り替えすことによって意図しない振る舞いが生じたことでした。
destroyされるのではなく、pauseのままでいて欲しいんだけどなーといろいろ悩んでいたのですが……

この問題はviewPagerとは異なるviewPager2のライフサイクルによって解決することになります。

##viewPager2を利用した場合のfragmentライフサイクル

viewPager2を利用するにあたり、先ほどのコードを修正します。

###アダプターの修正

FragmentStateAdapterオブジェクトを使用します。
viewPager2ではviewPagerのgetItem()とgetCount()に代わってcreateFragment()とgetItemCount()を実装する必要があります。

表示の制御を先ほどのリストからwhenに変えています。
（リストでやる方法もあるのかもしれませんが、すぐにわからなかったので今はwhenで返しています）

```kotlin:SamplePagerAdapter.kt
package your_package_name

import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter

class SamplePagerAdapter2(fm: FragmentActivity):FragmentStateAdapter(fm) {

    override fun createFragment(position: Int): Fragment  =
        when(position){
            0 -> Fragment1()
            1 -> Fragment2()
            2 -> Fragment3()
            else -> Fragment4()
        }

    override fun getItemCount(): Int {
        return 4
    }
}
```

###MainActivityの修正
```kotlin:MainActivity.kt
package your_package_name

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.viewpager2.widget.ViewPager2


class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        /// adapterのインスタンス生成
        val adapter = SamplePagerAdapter2(this)
        /// adapterをセット
        val viewPager2 = findViewById<ViewPager2>(R.id.viewPager2)
        viewPager2.adapter = adapter
    }
}
```

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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=".MainActivity">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager2"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
```

プログラムとレイアウトのどちらもviewPagerの部分をviewPager2にします。
未修整の部分があるとおそらくエラーが出るかと思います。


修正ができたら先ほどと同様にライフサイクルを調べます。


![viewpager2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/668653/7dd22bda-001f-aa95-ace5-b105ff025178.jpeg)

先ほどと違い、フラグメントの生成は初めて画面が表示された時であること、また**画面が表示されなくなるとpauseとなり、destroyされないことが確認できました！**


##まとめ
viewPagerでは表示されている両隣以外のフラグメントはdestroyされ、隣のフラグメントに来た時にcreateされます。一方、viewPager2では画面が初めて表示されるとフラグメントが生成され、表示中はresume、それ以外のフラグメントはpauseになるというライフサイクルの違いがありました。

影響がある場合はそんなに多くないかもしれませんが頭の片隅においておくと役に立つ時があるかもしれません。

何より冒頭にも書きましたがviewPagerは非推奨なので今後利用する場合はviewPager2を使うべきだと思います。

##参考文献
https://qiita.com/YS-BETA/items/091a84961d5b56fced2b