はじめに
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. 値の保存
値を保持する処理を書いていきます。
1. 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 |
2.
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: 保存した値に対応する出目の画像を表示させる
値を保存して、変数に格納することができたため、この変数を用いて画面に画像を表示させます。
1. MainActivity内に displaySavedDice(savedValue: Int)という関数を作成し、引数に savedValueを取ります。
private fun displaySavedDice(savedValue: Int) { } |
2. 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の値も更新する必要があります。
1. 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() } |
まとめ
以上が画面回転時の値の保持についての方法でした。今回まとめたもの以外にも方法は複数あると思いますので、興味がある方は調べてみてください。
最後まで読んでいただきありがとうございました!