カテゴリー
Kotlin言語

ゼロから始めるAndroidアプリ(Kotlin編)Unit3 – 1 第4回 後編:ライフサイクルとコンフィグチェンジ

Android Developers > スタートガイド > Kotlin と Android をゼロから学ぶ > Unit3 – ナビゲーションUnit3-1 第4回 後編 の内容をまとめたものです(翻訳ではありません)


Unit3 – 1 第4回 後編

コンフィグチェンジ(configuration change)が起きたときのライフサイクル

コンフィギュレーション・チェンジは長すぎるので、コンフィグチェンジって呼んじゃいますね

  • コンフィグチェンジ(configuration change)は、Androidデバイスの状態が急に大きく変わると発生します
  • Androidデバイスの状態が急に変更されてしまい、変更に対応するには今あるアクティビティを破棄して新たに生成し直した方がよいとシステムが判断することがあります
    • ユーザがデバイスの言語を変更したとき:テキストの方向や文字列の長さが変わってしまうので、システムはすべてのレイアウトを変更しようとします
    • ユーザがデバイスをドックに差し込んだり、物理キーボードを接続したりしたとき:システムは異なるディスプレイサイズやレイアウトに対応しようとします
    • デバイスを回転させて方向(縦横)が変更されたとき:レイアウトを新しい方向に合わせて変更しようとします
  • コンフィグチェンジはアクティビティのライフサイクルに影響します
  • アクティビティのライフサイクルが実際にどのように変化するのか見てみます

デバイスを回転する

デバイスの画面を回転するとコンフィグチェンジが発生します

  • アプリをビルドして実行します
  • デバイスをランドスケープモードに回転してみます(デバイスの設定で回転を有効にしておきます)
  • Logcat を見てみます。”D/MainActivity” でフィルタリングしておきます
2020-10-16 11:03:09.618 23206-23206/com.example.android.dessertclicker D/MainActivity: onCreate Called
2020-10-16 11:03:09.806 23206-23206/com.example.android.dessertclicker D/MainActivity: onStart Called
2020-10-16 11:03:09.808 23206-23206/com.example.android.dessertclicker D/MainActivity: onResume Called
2020-10-16 11:03:24.488 23206-23206/com.example.android.dessertclicker D/MainActivity: onPause Called
2020-10-16 11:03:24.490 23206-23206/com.example.android.dessertclicker D/MainActivity: onStop Called
2020-10-16 11:03:24.493 23206-23206/com.example.android.dessertclicker D/MainActivity: onDestroy Called
2020-10-16 11:03:24.520 23206-23206/com.example.android.dessertclicker D/MainActivity: onCreate Called
2020-10-16 11:03:24.569 23206-23206/com.example.android.dessertclicker D/MainActivity: onStart Called

画面を回転させると、アプリはいったん終了させられちゃうんですね

  • Androidデバイスの画面を回転させると、システムはアクティビティを破棄(Destroyed)する方向で、ライフサイクルのコールバックを次々と呼び出します
  • 破棄されたあと、システムによってアクティビティは新たに生成され、再び画面表示されるようにライフサイクルのコールバックが呼び出されています
  • アクティビティがいったん破棄され再生成されるため、値が初期化されてしまいます。デザートの売り上げ個数や金額がゼロにリセットされています

状態を保存しておく

システムの判断でアプリを終了させるときは、その前に設定を保存するチャンスがあるの

  • onSaveInstanceState() メソッドは、アクティビティが破棄(destroyed)されてしまったときに復元に必要なデータを保存するのにつかうコールバックです
  • onSaveInstanceState() は onStop() のあとに呼ばれます
  • アプリがバックグラウンドに移動するたびに呼ばれます

えっと、処理は onSaveInstanceState() の中に書いておくんですね

                         onRestoreInstanceState
                onRestart →
 [Initialized] → onCreate → [Created] → onStart → [Started] → onResume → [Resumed]
 [Destroyed]   ←                       ← onStop  ←           ← onPause  ←
                    onSaveInstanceState
   メモリ上                  オブジェクト              画面表示                 操作可能
  • onSaveInstanceState() はシステムによってアクティビティが破棄されてしまったときの安全対策として利用します
  • アクティビティがフォアグラウンドを去るとき、ちょっとした情報をまとめて保存しておくことができます。システムはこのデータをすぐに保存します

ポイント

  • Androidは、システムに負荷がかかったり、表示が遅れてしまう恐れがあったりするとバックグラウンドにあるアクティビティを含むプロセスを何の警告もなしに終了させてしまいます
  • ただしアプリはユーザからは終了しているようには見えません。ユーザが再びアプリを操作しようとすると Android はアプリを再起動します
  • このような場合にアプリのデータが失われないように、onSaveInstanceState() で毎回データを保存しておきます

onSaveInstanceState() をオーバーライドする

onSaveInstanceState() はオーバーライドで実装するの

MainActivity クラスで onSaveInstanceState() コールバックをオーバーライドして、ログ出力の命令を追加します

override fun onSaveInstanceState(outState: Bundle) {
   super.onSaveInstanceState(outState)

   Log.d(TAG, "onSaveInstanceState Called")
}

onSaveInstanceState() について

  • onSaveInstanceState() の名前でオーバーライドできるものには2種類あります
  • コンストラクタのパラメータが1つのものと、2つのものです
  • 1つのもののパラメータは outState、2つのものは outState と outPersistentState です
  • ここではパラメータが1つ(outState)のものを使います

コールバックの呼び出しを確認する

Logcat で onSaveInstanceState() が呼び出されているのを確認しておきますね

  • アプリをビルドして実行します
  • デバイスの Home ボタンをタップしてアプリをバックグラウンドに回します
  • onSaveInstanceState() コールバックが onPause() と onStop() のあとに発生しているのを確認します
2020-10-16 11:05:21.726 23415-23415/com.example.android.dessertclicker D/MainActivity: onPause Called
2020-10-16 11:05:22.382 23415-23415/com.example.android.dessertclicker D/MainActivity: onStop Called
2020-10-16 11:05:22.393 23415-23415/com.example.android.dessertclicker D/MainActivity: onSaveInstanceState Called

バンドルにデータを保存する

アクティビティの間でデータをやりとりするのにはバンドルを使うの

ファイルの先頭、クラス定義のちょうど前に、これらの定数を追加します

const val KEY_REVENUE = "revenue_key"
const val KEY_DESSERT_SOLD = "dessert_sold_key"

バンドルに保存する値には「キー」を設定するの

キーは定数にしておくといいんですよね

  • これらのキーは、データをバンドルに保存したり取り出したりするのに使います
    • Bundle は、キーと値がペアになったコレクション(データ構造)で、キーは常に文字列です
    • バンドルは、Android のアクティビティの間でデータをやりとりするのに使います
  • onSaveInstanceState() にある outState パラメータをみると、型は Bundle になっています
  • バンドルには Int や Boolean のようなシンプルなデータを入れることができます
  • バンドルのサイズには限度があります(サイズはデバイスによって変わります)
  • 保存するデータが多すぎると、TransactionTooLargeException が発生してアプリがクラッシュする危険があります

outState に値を設定する

outState パラメータがバンドルなの

ここに保存するんですね

  • onSaveInstanceState() で、revenue(整数)の値を putInt() メソッドを使ってバンドルに保存します
outState.putInt(KEY_REVENUE, revenue)
  • putInt() メソッドは2つの引数をとります
    • ひとつはキーにする文字列です(ここでは KEY_REVENUE 定数)
    • もうひとつは実際に保存する値です
    • Bundleクラスには他にも putFloat() や putString() のようなよく似たメソッドがあります
  • デザートの売り上げ数についても同じように保存するようにします
outState.putInt(KEY_DESSERT_SOLD, dessertsSold)

保存できました

onCreate() でバンドルからデータを復元する

データを復元してみますね

  • アクティビティの状態は、onCreate(Bundle) または  onRestoreInstanceState(Bundle) の中で復元できます
    • onSaveInstanceState()メソッドによって生成されたバンドルはどちらのコールバックメソッドからでも参照できます
  • onCreate() のところに移動して、メソッドの署名(宣言)のところを確認します
override fun onCreate(savedInstanceState: Bundle) {

あっ、これバンドルですね

  • onCreate() は呼ばれるたびにバンドルを取得しています
  • プロセスがシャットダウンしてアクティビティが再起動するとき、保存されたバンドルが onCreate() に渡されます
  • アクティビティが完全に初めての起動の場合、onCreate() に渡されるバンドルは null です
  • バンドルが null でないときは、以前のどこかのポイントからアクティビティが再生成されています

ポイント

  • アクティビティが再生成される場合、onStart() のあとに onRestoreInstanceState() コールバックがバンドルを持った状態で呼ばれます
  • 多くの場合、アクティビティの状態の復元は onCreate() で行います
  • onRestoreInstanceState() は onStart() のあとに呼ばれるので、onCreate() が呼ばれたの後の何らかの状態をリストアする必要がある場合には onRestoreInstanceState() を使って復元します

データを復元するコードを記述する

データは getInt() で取り出しますね

onCreate() に次のコードを加えます。変数 binding がセットされているちょうど後ろに挿入します

if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
   dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
}
  • revenue(総額)と dessertsSold(デザートの売り上げ数)を復元しています
  • バンドルにデータがあるか(null かどうか)確認して、アプリが初めて起動されたのか、シャットダウン後に再生成されたのかを判断します
  • これはバンドルからデータを復元するときによく使うパターンです
  • バンドルからのデータ取得には getInt() を使います
  • getInt() メソッドは2つの引数をとります
    • キーとして使う文字列。たとえば “key_revenue” は整数 revenue のためのキーです
    • デフォルト値。バンドルの中にそのキーに対応する値が存在しない場合に使うデフォルト値です
  • 取得のためのキーは、バンドルへのデータ保存時に putInt() で使ったのと同じもの(KEY_REVENUE)を使います
  • 同じキーを間違いなく使うために、キーを定数として定義しておきます

アプリを実行してテストする

ちゃんとうごくかな?

  • アプリをビルドして実行します
  • デザート画像がドーナツに変わるまで、少なくとも5回はカップケーキをタップします
  • デバイスを回転してみます
  • 画面の総額(revenue)とデザートの売り上げ数(dessertsSold)が正しく表示されていることを確認します

残っている問題点

あとちょっと直します

  • デバイスを回転すると、ドーナツだったデザート画像がカップケーキに戻ってしまっています
  • デバイスを回転したあとも正しい画像が表示されるように、デザート画像についても復元するコードを記述します

デザート画像が更新されるようにする

画面にデザート画像を表示している関数を見ておきますね

  • MainActivity クラスの中にある showCurrentDessert() メソッドを探します
  • このメソッドは、デザートの販売数に応じて正しいデザート画像を表示するようにアプリを設定します
  • アクティビティに表示する画像は、現在のデザート販売数と変数 allDesserts の情報(デザート画像,価格,販売条件をまとめたリスト)で決定しています
  • 販売中のデザート(newDessert)は次のようなコードで設定されています
for (dessert in allDesserts) {
   if (dessertsSold >= dessert.startProductionAmount) {
       newDessert = dessert
   }
    else break
}

これで今日のデザートを決めてるんですね

showCurrentDessert() を使って画像を更新する

数値がちゃんと復元されたらあらためてデザート画像を描画するの

  • MainActivity クラスの onCreate() メソッドに移動します
  • バンドルから状態を復元するブロックの中に showCurrentDessert() を記述します
 if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
   dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
   showCurrentDessert()                   
}

アプリを実行して確認する

  • アプリをビルドして実行します
  • 画面を回転させてみます
  • デザートの売り上げ数、総額、デザート画像が正しく復元されていることを確認します

ちゃんと動きました!

まとめ

さいごにまとめますね

アクティビティの状態を保存する

アクティビティのデータはバンドルで受け渡しできます

  • onStop() が呼ばれてアプリがバックグラウンドに移動するとき、アプリのデータはバンドルに保存できます
    • データによって(たとえば EditText の内容など)は、自動的に保存されるようになっています
  • バンドルは Bundle クラスのインスタンスです。キーと値がペアになったコレクションで、キーは常に文字列です
  • アプリがAndroidによっていつのまにかシャットダウンさせられてしまったときに備えて、アプリの復元に必要なデータを onSaveInstanceState() コールバックを使ってバンドルに保存しておきます
  • バンドルにデータを保存するには、Bundle に用意されている put ではじまるメソッド(たとえば putInt() など)を使います
  • バンドルからデータを取り出して復元するには onRestoreInstanceState() メソッド、あるいは onCreate() メソッド(こちらの方が一般的によく使う)を使います
  • onCreate() メソッドには savedInstanceState パラメータが用意されていて、ここにバンドルが設定されています
  • アクティビティが完全に初めての起動の場合、onCreate() のsavedInstanceState パラメータは null です
  • バンドルからデータを取得するには、Bundle に用意されている get ではじまるメソッド(たとえば getInt() など)を使います

デバイスの設定が変更されたとき

コンフィグチェンジが起きたときの備えも大事なんですね

  • コンフィグチェンジ(configuration change)は、デバイスの状態が急に変化して、システムが現在のアクティビティを破棄して新たに生成し直した方がよいと判断したときに発生します
  • コンフィグチェンジのもっとも一般的な例はユーザがデバイスを回転させてポートレートからランドスケープモードにしたり、逆にランドスケープからポートレートモードにしたときです
  • ほかにもデバイスの言語を変更したとき、ハードウェアキーボードを接続したときなどにも起こります
  • コンフィグチェンジが起こると、Android はすべてのアクティビティライフサイクルのシャットダウンを行った上で、アクティビティを最初から起動し直します
  • コンフィグチェンジによってアプリがシャットダウンした場合、システムはバンドルが設定されたアクティビティを再起動します。バンドルは onCreate() で利用できます
  • シャットダウンされるとき、アプリの状態は onSaveInstanceState() の中でバンドルに保存できます

Androidデバイスでの操作

よくある操作でのアクティビティのライフサイクルの変化をまとめておきますね

  • ◀・・・完全に破棄されます(Destroyed)
    • onSaveInstanceState は呼ばれません
    • バンドルは null に初期化されます
  • ●・・・ホームメニューを表示(Createdで停止)
    • 停止前に onSaveInstanceState が呼ばれます
  • ■・・・「最近の画面」を表示(Createdで停止)
    • 停止前に onSaveInstanceState が呼ばれます
  • 画面を回転させる
    • onPause -> onStop
    • onSaveInstanceState が呼ばれる
    • onDestroy -> 破棄(Destroyed)
    • onCreate -> onStart
    • onRestoreInstanceStateが呼ばれる
    • onResume

おつかれさまでした

参考