はじめに
今回は画面を回転させたときや本体をスリープさせた後、ビューの一部がその前後で変わってしまう問題について取り組みます。下の簡易的なボタンカウンターアプリでその様子が確認できます。縦画面でカウントアップさせたとき、横に切り替えると数字が0に戻ってしまうのです。
原因
この問題の原因は、画面の向きを変えるなどの諸動作によって、Activityがライフサイクルを一からやり直す事にあります。これによってハードウェアは、柔軟にかつ素早くユーザーのアクションを反映させることが出来ますが、同時にUIまでもが破棄されて初期化されてしまいます。これを防ぐためには、ライフサイクルが終わる前に、UI等の必要な情報を一時的に保持しておく必要があります。
*ライフサイクルについてはこちら:https://qiita.com/K4N4/items/2f4babe2bab67ddacf89
onSaveInstanceState
一時的に情報を保持する方法の一つに、onSaveInstanceStateを利用するというものがあります。この方法はあまり大容量ではない情報を、簡単に保持する時に利用します。使い方は非常にシンプルです。
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState?.putInt("key", i)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
i = savedInstanceState?.getInt("key", 0)
textView.text = i.toString()
}
onSaveInstanceState
メソッド(データを保持する)とonRestoreInstanceState
メソッド(保持したデータを復元する)を呼び出し、put〇〇
とget〇〇
で実際にデータの受け渡しを行っています。
onSaveInstanceState
メソッドのoutState?.putInt("key", i)
というのはoutState
(データをメモリに保存する実体)に"key"(任意)というキーを使って、iというIntの値を保持する。という事を指しています。キーとはその名の通り鍵の事です。呼び出し側と共通のキーを持たなければ、データの受け渡しは出来ません。
onRestoreInstanceState
メソッドのi = savedInstanceState?.getInt("key", 0)
は、Activityが破棄されて値が初期化されてしまった i に、保持したデータの実態savedInstanceState
からgetInt
メソッドで共通のキーを持つ値を取り出しています。getInt
の第二引数は、putInt
がnullの場合の値を設定します。
この二つのメソッドとその中身を入れるだけで、簡単に設定した値の保持が行えます。
onSaveInstanceStateとonRestoreInstanceStateとライフサイクル
先述したようにこの二つのメソッドを使えば、簡単にUIの保持を行えます。では、この二つのメソッドはActivityのライフサイクルにおいて、どの段階で機能してるのでしょうか?
それを示すのがこの図です。この図はActivityの基本的な工程に、今回用いた二つのメソッド等を加えたものです。この図の通り、onSaveInstanceState
はonPauseの後に値の保持を、onRestoreInstanceState
はonStart
の後に値の復元を行っています。onRestoreInstanceState
の値はonCreate
に影響を及ぼさなかったり、onSaveInstanceState
が呼び出されたら必ずアプリが止まることなどが分かります。
実際にこのライフサイクルを可視化してみる
class MainActivity : AppCompatActivity() {
private var i : Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
Log.d(TAG, "onStart: called")
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
i += 1
textView.text = i.toString()
}
}
override fun onStart() {
Log.d(TAG, "onStart: called")
super.onStart()
}
override fun onResume() {
Log.d(TAG, "onResume: called")
super.onResume()
}
override fun onPause() {
Log.d(TAG, "onPause: called")
super.onPause()
}
override fun onSaveInstanceState(outState: Bundle) {
Log.d(TAG, "onSaveInstanceState: called")
super.onSaveInstanceState(outState)
outState.putInt("key", i)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
Log.d(TAG, "onRestoreInstanceState: called")
super.onRestoreInstanceState(savedInstanceState)
i = savedInstanceState.getInt("key", 0)
textView.text = i.toString()
}
override fun onStop() {
Log.d(TAG, "onStop: called")
super.onStop()
}
override fun onRestart() {
Log.d(TAG, "onRestart: called")
super.onRestart()
}
override fun onDestroy() {
Log.d(TAG, "onDestroy: called")
super.onDestroy()
}
}
MainActivityの中身をこのようにすることで、実際にどこでデータの保持等が行われているかログを通して可視化することが出来ます。このソースコードでは、上で示したカウントアップ機能のためにデータのやり取りを行っています。上手くいけば、各工程ごとにログに出力がなされます。是非回転などさせて試してみてください。