横スワイプによる画面遷移アプリを作成するために、viewPagerを利用していたのですが、ライフサイクルに関する思わぬ沼にはまり苦労しました。
そこで、viewPagerとviewPager2をそれぞれ利用してfragmentをスワイプで遷移する場合のfragmentのライフサイクルについてまとめました。
##はじめに
現在ではviewPagerは非推奨となっており、viewPager2を使うようにとのこと。viewPagerでのいくつかの問題が修正されているほか、アクティブな開発サポートを受けられることや、垂直方向のスワイプ遷移等ができるそう。
スワイプ機能のあるアプリを作ろうと検索するとviewPagerを利用したものばかりが引っ掛かったのでその点は注意。
詳しくはdeveloperのページを参照してください。
##fragmentのライフサイクル
画像: Androidデベロッパー フラグメントより
以下に主なコールバックメソッドを簡単に説明します。
より詳しい内容については以下のサイトをご覧ください。
###onCreate()
フラグメントが生成されるときに呼び出される。
###onCreateView()
画面を描画、viewを生成する。
###onViewCreated()
viewが生成されたら呼び出される。viewの初期化やフラグメントの状態の復元はここで行うほうがよい。
###onReseme()
まさにスマホの画面に表示されたとき、つまりユーザーが操作を行えるようになる直前にに呼び出される。
###onPause()
画面に表示されなくなる、ユーザーが操作できなくなる時に呼び出される。
###onDestroy()
フラグメントが破棄される
##スワイプでfragmentを遷移するコード
フラグメントのライフサイクルを調べるために、まずはviewPagerを利用してスワイプで画面遷移ができるアプリを作成します。
上の図はアプリのアクティビティとフラグメントの構成を示したものです。
各画面を表示させるのはフラグメントで、どのフラグメントを表示させるか制御するのがアダプター、そしてそのアダプターはMainActivity上に生成されます。
###フラグメントの生成
最初に画面となるフラグメントを生成。
File > New > Fragment > Fragment(Blank)を選択し、名前を入力。するとFragmentクラスとそのレイアウトファイルが生成されます。
Fragmentファイルにはいろいろ書かれていますが、onCreateView以外の部分は消して問題ないです。(画面生成をするonCreateViewは必須)
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 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は必須で、それぞれ表示するフラグメントの制御と、コンテンツのサイズ(数)をセット。
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の生成
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 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>
できあがったアプリはこんな感じでスワイプでフラグメントを変えられます。
##viewPagerを利用した場合のfragmentライフサイクル
アプリが出来たので実際にフラグメントのライフサイクルを見ていきたいと思います。
各フラグメントにon~のコールメソッドが呼び出されたときにSystemに出力させるようにして状態をチェックします。
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に戻る場合を調べます。
黄色マス:表示画面
青マス:フラグメント生成
灰色マス:フラグメント破棄
上記から表示されているフラグメントの隣のフラグメントは生成されていることがわかります。
また画面を移ったときに、隣のフラグメントであればpuaseになりますが、隣以外のフラグメントは破棄されていることがわかります。
今回困ったことはこのフラグメントを破棄⇒生成を繰り替えすことによって意図しない振る舞いが生じたことでした。
destroyされるのではなく、pauseのままでいて欲しいんだけどなーといろいろ悩んでいたのですが……
この問題はviewPagerとは異なるviewPager2のライフサイクルによって解決することになります。
##viewPager2を利用した場合のfragmentライフサイクル
viewPager2を利用するにあたり、先ほどのコードを修正します。
###アダプターの修正
FragmentStateAdapterオブジェクトを使用します。
viewPager2ではviewPagerのgetItem()とgetCount()に代わってcreateFragment()とgetItemCount()を実装する必要があります。
表示の制御を先ほどのリストからwhenに変えています。
(リストでやる方法もあるのかもしれませんが、すぐにわからなかったので今はwhenで返しています)
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の修正
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 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にします。
未修整の部分があるとおそらくエラーが出るかと思います。
修正ができたら先ほどと同様にライフサイクルを調べます。
先ほどと違い、フラグメントの生成は初めて画面が表示された時であること、また画面が表示されなくなるとpauseとなり、destroyされないことが確認できました!
##まとめ
viewPagerでは表示されている両隣以外のフラグメントはdestroyされ、隣のフラグメントに来た時にcreateされます。一方、viewPager2では画面が初めて表示されるとフラグメントが生成され、表示中はresume、それ以外のフラグメントはpauseになるというライフサイクルの違いがありました。
影響がある場合はそんなに多くないかもしれませんが頭の片隅においておくと役に立つ時があるかもしれません。
何より冒頭にも書きましたがviewPagerは非推奨なので今後利用する場合はviewPager2を使うべきだと思います。