はじめに
NX開発推進部Androidアプリ開発グループ所属の深沢と申します。普段はAndroidアプリの開発業務に携わっています。
スマホを使用していると、ふとした時にスマホが傾いてしまい、画面の向きが変わってしまうことがあるかと思います。その時に下のgifのようにアプリの内容がリセットされたらとても不便だと思いませんか?

Androidアプリはとある処理を追加しないと、画面回転時に値が保持されないようになっています。 ここでは、その値保持の処理についてまとめました。KotlinやAndroid Studioの基本的な使い方は知っている前提で話を進めています。
今回使用するコード
今回使用するコードは以下のとおりです。ボタンを押すとサイコロの出目が変わるというシンプルなものになっています。
<!-- 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"
android:importantForAccessibility="no"
tools:context=".MainActivity">
<Button
android:id="@+id/diceButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/roll"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/diceImage" />
<ImageView
android:id="@+id/diceImage"
android:layout_width="160dp"
android:layout_height="200dp"
android:contentDescription="dice"
android:importantForAccessibility="no"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@drawable/dice_1" />
</androidx.constraintlayout.widget.ConstraintLayout>
// MainActivity.kt
package com.hinako.fukasawa.diceroller
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.ImageView
class MainActivity : AppCompatActivity() {
private var diceRoll: Int = 1
private val dice = Dice(6)
private lateinit var diceImage: ImageView
private lateinit var rollButton: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
rollButton = findViewById(R.id.diceButton)
diceImage = findViewById(R.id.diceImage)
diceImage.setImageResource(dice.display(diceRoll))
rollButton.setOnClickListener {
rollDice()
}
}
// 出目に合わせた画像を表示する
private fun rollDice() {
diceRoll = dice.roll()
diceImage.setImageResource(dice.display(diceRoll))
// 画像の説明
diceImage.contentDescription = diceRoll.toString()
}
}
class Dice(private val numSides: Int) {
fun roll(): Int {
return (1..numSides).shuffled().first()
}
fun display(diceRoll: Int): Int {
return when (diceRoll) {
1 -> R.drawable.dice_1
2 -> R.drawable.dice_2
3 -> R.drawable.dice_3
4 -> R.drawable.dice_4
5 -> R.drawable.dice_5
else -> R.drawable.dice_6
}
}
}
上記コードを実行しますと、画面を回転したときに出目の値が1にリセットされてしまいます。

これから、画面を回転させた時でも内容をリセットさせないようにする方法を2つほど紹介致します。
方法1: 値を保存して、アクティビティの再作成時に取得する
実装手順
以下の5ステップで実装していきます。
- ログの確認
- 値の保存
- 値の取得
- 保存した値に対応する出目の画像を表示させる
- 値の保存を維持する
ステップ1: ログの確認(省略可)
画面を回転させたときのライフサイクルに関するログを確認します。
2022-12-12 12:52:49.459 24279-24279/com.example.android.dessertclicker I/MainActivity: onPause Called
2022-12-12 12:52:49.461 24279-24279/com.example.android.dessertclicker I/MainActivity: onStop Called
2022-12-12 12:52:49.463 24279-24279/com.example.android.dessertclicker I/MainActivity: onDestroy Called
2022-12-12 12:52:49.484 24279-24279/com.example.android.dessertclicker I/MainActivity: onCreate called
2022-12-12 12:52:49.502 24279-24279/com.example.android.dessertclicker I/MainActivity: onStart called
2022-12-12 12:52:49.503 24279-24279/com.example.android.dessertclicker I/MainActivity: onResume Called
ログを確認すると、
onDestroyでアクティビティが破棄された後に、onCreate()で再び作成されていることがわかります。
ステップ2. 値の保存
値を保持する処理を書いていきます。
override fun onSaveInstanceState(outState: Bundle)をMainActivity内に追加します。
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
}
onSaveInstanceState()は状態の保存を行う関数です。この関数のログも追加して、画面を回転させるとonSaveInstanceState()はアクティビティが停止した後に呼び出されることがわかります。この関数はアプリがバックグラウンドに移行するたびに呼び出されます。
2022-12-12 12:52:49.459 24279-24279/com.example.android.dessertclicker I/MainActivity: onPause Called
2022-12-12 12:52:49.461 24279-24279/com.example.android.dessertclicker I/MainActivity: onStop Called
2022-12-12 12:52:49.462 24279-24279/com.example.android.dessertclicker I/MainActivity: onSaveInstanceState Called
2022-12-12 12:52:49.463 24279-24279/com.example.android.dessertclicker I/MainActivity: onDestroy Called
2022-12-12 12:52:49.484 24279-24279/com.example.android.dessertclicker I/MainActivity: onCreate called
2022-12-12 12:52:49.502 24279-24279/com.example.android.dessertclicker I/MainActivity: onStart called
2022-12-12 12:52:49.503 24279-24279/com.example.android.dessertclicker I/MainActivity: onResume Called
Bundleというデータを保管してくれるクラスに値を保存する時に、キーを使用するので設定します。 キーをクラス宣言の前にconst valとして設定します。今回はKEY_DICEとしました。
const val KEY_DICE = "dice_key"
3.値を保存させる処理を
onSaveInstanceState()内に書いていきます。
outState.putInt(KEY_DICE, diceRoll)
ステップ3: 値の取得
ステップ2で保存した値を
onCreate()で取得します。
1.再作成時は
onCreate()から走るため、onCreate()内にif (savedInstanceState != null)と書きます。
if (savedInstanceState != null) {
}
これで
Bundle内に値があるときのみコードを走らせるようにしました。
2.1のif文の中に以下を追記します。
savedValue = savedInstanceState.getInt(KEY_DICE)
保存したときと同じキーを用いて、
savedValueに値を代入します。しかし、このままだと未定義エラーが出てしまうため、修正していきます。
3.クラス宣言直下に保存されている値を入れる変数を定義します。値は初期状態の値である
1としました。
private var savedValue = 1
これで未定義エラーは消えました。
ステップ4: 保存した値に対応する出目の画像を表示させる
値を保存して、変数に格納することができたため、この変数を用いて画面に画像を表示させます。
MainActivity内にdisplaySavedDice(savedValue: Int)という関数を作成し、引数にsavedValueを取ります。
private fun displaySavedDice(savedValue: Int) {
}
displaySavedDice()内にrollDice()と同じ要領で、savedValueの出目に対応した画像を表示させる処理を書きます。
private fun displaySavedDice(savedValue: Int) {
diceImage.setImageResource(dice.display(savedValue))
diceImage.contentDescription = savedValue.toString()
}
3.ステップ3で作成した
savedValueの下にdisplaySavedDice(savedValue)を追加します。
if (savedInstanceState != null) {
savedValue = savedInstanceState.getInt(KEY_DICE, 1)
displaySavedDice(savedValue)
}
これで、アクティビティを破棄した際にも値が保存されるようになりました。
4.アプリを実行し、確認します。回転させても出目が保存されるようになりました。
しかし、もう一度回転させると、出目が1に変更されてしまいます。

ステップ5: 値の保存を維持する
出目が戻る原因を確認します。ステップ1のログより画面を回転させるとActivityが再作成されるため、
onCreate()が再び呼ばれることがわかります。
上から順に処理していき、
if (savedInstanceState != null)...に到着します。ここでsavedValueにはアクティビティ破棄前に保存されていた値が代入されるのですが、diceRollには代入されません。そのため、diceRollの値は初期値の1となります。
class MainActivity : AppCompatActivity() {
private var savedValue: Int = 1
private var diceRoll: Int = 1
private val dice = Dice(6)
private lateinit var diceImage: ImageView
private lateinit var rollButton: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
rollButton = findViewById(R.id.diceButton)
diceImage = findViewById(R.id.diceImage)
rollButton.setOnClickListener {
rollDice()
}
// Bundleがnullではない時
if (savedInstanceState != null) {
// 値を取り出す
savedValue = savedInstanceState.getInt(KEY_DICE)
displaySavedDice(savedValue)
}
このまま再び画面を回転させるとどうなるでしょうか?コードは
fun onSaveInstanceState(outState: Bundle)に到達します。ここでdiceRollの値を保存するのですが、現在diceRollには1が入っているため、出目は何であれ1が保存されてしまうことがわかるかと思います。
そこで、
saveValueと同じところでdiceRollの値も更新する必要があります。
onCreate()のif文内でdiceRollの値を更新します。
savedValue = savedInstanceState.getInt(KEY_DICE)
diceRoll = savedInstanceState.getInt(KEY_DICE)
2.再び実行し、画面を複数回回転させても出目が保持されていることを確認します。

完成コード
完成した
MainActivity.ktは以下のとおりです。(activity_main.xmlは変更していないため割愛しています)
package com.hinako.fukasawa.diceroller
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.ImageView
const val KEY_DICE = "dice_key"
class MainActivity : AppCompatActivity() {
private var savedValue = 1
private var diceRoll: Int = 1
private val dice = Dice(6)
private lateinit var diceImage: ImageView
private lateinit var rollButton: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
rollButton = findViewById(R.id.diceButton)
diceImage = findViewById(R.id.diceImage)
diceImage.setImageResource(dice.display(diceRoll))
rollButton.setOnClickListener {
rollDice()
}
// Bundleがnullではない時
if (savedInstanceState != null) {
// 値を取り出す
savedValue = savedInstanceState.getInt(KEY_DICE)
diceRoll = savedInstanceState.getInt(KEY_DICE)
displaySavedDice(savedValue)
}
}
// 出目に合わせた画像を表示する
private fun rollDice() {
diceRoll = dice.roll()
diceImage.setImageResource(dice.display(diceRoll))
// 画像の説明
diceImage.contentDescription = diceRoll.toString()
}
private fun displaySavedDice(savedValue: Int) {
diceImage.setImageResource(dice.display(savedValue))
diceImage.contentDescription = savedValue.toString()
}
// 値を保持
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(KEY_DICE, diceRoll)
}
}
class Dice(private val numSides: Int) {
fun roll(): Int {
return (1..numSides).shuffled().first()
}
fun display(diceRoll: Int): Int {
return when (diceRoll) {
1 -> R.drawable.dice_1
2 -> R.drawable.dice_2
3 -> R.drawable.dice_3
4 -> R.drawable.dice_4
5 -> R.drawable.dice_5
else -> R.drawable.dice_6
}
}
}
方法2: 画面回転時にアクティビティを破棄させない
方法1では「アクティビティを再作成したときに、破棄前と同じ状態になるように処理する」という考え方でしたが、方法2では「画面回転時にそもそもアクティビティを破棄しないようにする」という考えのもと作業を行います。
実装方法
こちらは
AndroidManifest.xmlにandroid:configChanges="orientation|screenSize"という一文を追加するだけです。
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize"
android:exported="true">
実行すると、方法1と同様に画面を回転させても値が変更されません。
ログを確認してみます。
2022-12-20 12:13:11.625 13900-13900/com.hinako.fukasawa.diceroller I/Main: onCreate
画面を回転させてもアクティビティが破棄されていないことが確認できます。
この
android:configChangesを設定した場合、画面回転時にonConfigurationChangedがコールバックされます。そのため、このメソッドをオーバーライドすると、画面回転時の処理を記述することができます。
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// 任意の処理
}
以下のように書くと画面回転時にトーストメッセージが表示されるようになります。
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
Toast.makeText(applicationContext, "Screen rotated!", Toast.LENGTH_LONG).show()
}

まとめ
以上が画面回転時の値の保持についての方法でした。今回まとめたもの以外にも方法は複数あると思いますので、興味がある方は調べてみてください。
最後まで読んでいただきありがとうございました!
参考
深沢雛子