【Kotlin】onSaveInstanceStateで画面回転時の状態の保存と復元【Androidアプリ開発】
今回のAndroidサンプルプロジェクトでは、フラグメントを2つ作り、onSaveInstanceStateを使ったアクティビティの状態の復元を行った。
ButtonFragment
ButtonFragmentを次のようにして作成した。class ButtonFragment : Fragment() {
private var listener: OnFragmentInteractionListener? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_button, container, false)
view.findViewById<Button>(R.id.button)
.setOnClickListener {
listener?.onButtonClicked()
}
return view
}
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is OnFragmentInteractionListener) {
listener = context
} else {
throw RuntimeException(context.toString() + " must implement OnFragmentInteractionListener")
}
}
override fun onDetach() {
super.onDetach()
listener = null
}
interface OnFragmentInteractionListener {
fun onButtonClicked()
}
}
TextFragment
TextFragmentはButtonFragmentでボタンがクリックされる毎にカウントアップを表示させるためのフラグメントだ。また、カウンターはファクトリーメソッドを使ってホストアクティビティから初期値を設定できるようにしている。private const val COUNTER_KEY = "counter"
class TextFragment : Fragment() {
private var mCounter = 0
private lateinit var counterTextView: TextView
fun update() {
mCounter++
counterTextView.text = mCounter.toString()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
mCounter = it.getInt(COUNTER_KEY)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_text, container, false)
counterTextView = view.findViewById(R.id.textView)
counterTextView.text = mCounter.toString()
return view
}
companion object {
@JvmStatic
fun newInstance(counter: Int) =
TextFragment().apply {
arguments = Bundle().apply {
putInt(COUNTER_KEY, counter)
}
}
}
}
Fragmentをレイアウトファイルに設置する
今回はButtonFragmentを直接レイアウトファイルに設置し、TextFragmentはMainActivityから動的に設置する。
まずButtonFragmentを設置しよう。Paletteからfragmentを探してドラッグアンドドロップする。
<fragment
android:layout_width="0dp"
android:layout_height="0dp" android:name="com.apppppp.trysavedinstancestate.ButtonFragment"
android:id="@+id/buttonFragment" android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="@+id/guideline2" app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp" app:layout_constraintBottom_toBottomOf="parent"
tools:layout="@layout/fragment_button"/>
図のようにlayout属性に@layout/fragment_buttonを忘れずに指定しよう。これを設定しないとコンパイルエラーとなってしまうのだ。
つぎにTextFragmentを動的に追加するためFrameLayoutを使ったコンテナを作成する。
<FrameLayout
android:layout_width="0dp"
android:layout_height="0dp" app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="8dp" app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp" android:layout_marginEnd="8dp" app:layout_constraintEnd_toEndOf="parent"
android:id="@+id/container" android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="@+id/buttonFragment">
</FrameLayout>
このコンテナに動的にTextFragmentを追加するプログラムを見てみよう。MainActivityを次のように記述する。
private const val TEXT_FRAGMENT_TAG = "textFragment"
class MainActivity : AppCompatActivity(), ButtonFragment.OnFragmentInteractionListener {
override fun onButtonClicked() {
val fragment = supportFragmentManager.findFragmentByTag(TEXT_FRAGMENT_TAG) as TextFragment
fragment.update()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (supportFragmentManager.findFragmentByTag(TEXT_FRAGMENT_TAG) == null) {
supportFragmentManager.beginTransaction()
.add(R.id.container, TextFragment.newInstance(100), TEXT_FRAGMENT_TAG)
.commit()
}
}
}
注目すべきところはfragmentにTAGをつけてsupportFragmentManager.findFragmentByTagのように、どこからでも参照可能なようにしている。もし返り値がnullであるならば、まだフラグメントが生成されていないことなので、トランザクションを使用してフラグメントを新規作成している。
override fun onButtonClicked() {
val fragment = supportFragmentManager.findFragmentByTag(TEXT_FRAGMENT_TAG) as TextFragment
fragment.update()
}
状態の保存
ここまでのプログラムを実際に実行してみよう。カウンターの初期値に100が表示され、ボタンをクリックする毎に1づつカウントアップされるのが確認できることだろう。
しかし画面を回転するとどうだろうか?カウントアップの表示が初期化されてしまった。
実は画面が回転されるとAndroidシステムがレイアウトの再ロードを行う必要があるために、アクティビティが破棄され再作成される仕様となっているのだ。詳しくは下記サイトを参考に。 アクティビティのライフサイクルについて - Androidデベロッパードキュメントガイド
そこで解決策の1つとしてonSaveInstanceState()コールバックメソッドを使った状態の保存を実装する。onSaveInstanceState()はアクティビティが破棄される前に呼び出されるメソッドだ。そのメソッド内でBundleオブジェクトを使った状態の保存を行う。保存した値はonCreate()またはonRestoreInstanceState()で受け取ることができる。
今回はカウンターの値を保存しておきたいので、TextFragmentに次のようにonSaveInstanceState()を実装した。
override fun onSaveInstanceState(outState: Bundle) {
outState.putInt(COUNTER_KEY, mCounter)
super.onSaveInstanceState(outState)
}
そしてonCreateメソッドを次のように書き換えた。
mCounter = savedInstanceState?.getInt(COUNTER_KEY)
?: arguments?.getInt(COUNTER_KEY)
?: 0
エルビス演算子を使ってsavedInstanceStateに値がなければargumentsから値を呼び出し、それでも値がなければカウンターに初期値として0を設定している。
この状態でプログラムを再度実行してみよう。ボタンを何回か押してカウントアップさせたあと画面を回転してみるとどうなるだろうか?見事、カウントの値を保持したまま再表示させることができた。
まとめ
最後にこの記事で以下の内容を行ってきたことをおさらいしておこう。
- フラグメントからイベントをホストアクティビティに通知した。
- ファクトリーメソッドを使ってフラグメントへ初期値を渡した。
- フラグメントをTAGを使って管理した。
- アクティビティからフラグメントを動的に追加した。
- onSaveInstanceStateを使って画面回転時における状態の保存と復元を行った。
▼ こんな記事も書いてます。