Android Kotlin基礎講座 04.2: 複雑なライフサイクル状態
目次
タスク:アプリのシャットダウンとをシミュレートし、onSaveInstanceState()を使う
アプリがバックグラウンドにあるときに、Androidによってアプリがシャットダウンされてしまったら、アプリとアプリのデータには何が起こるのでしょうか。このような特定の状況も理解するのには重要です。
アプリがバックグラウンドにあるとき、それは破棄されたわけではありません。ただ低sいされ、ユーザーがアプリに戻ってくるのを待機しているのです。しかしAndroid OSはフォアグラウンドにあるアクティビティをスムーズに動作させることを第一に考えます。例として、もしユーザーがバスに乗るためにGPSアプリを使う場合、GPSアプリの描写や方向を示すのを素早く行うことが重要となります。バックグラウンドで2,3日放置されていたDessertClickerアプリをキープしておくことはあまり重要でないと考えられます。
Androidはバックグラウンドにあるアプリを規制し、フォアグラウンドにあるアプリが問題なく動作するように制御しています。例として、Androidはバックグラウンドで動作するアプリが行うことができる処理の量を制限しています。
時にはAndroidはアプリのプロセス全体をシャットダウンすることもあります。それにはアプリに関連するすべてのアクティビティが含まれます。システムが圧迫され、目に見えるほどのラグが発生しそうな場合に、Androidはこの種のシャットダウンをバックグラウンドで静的に実行します。ユーザーにはアプリが閉じられたことはわかりません。ユーザーがAndroidによって閉じられたアプリに再び戻ってきた場合、Androidはそのアプリを再起動します。
このタスクでは、Androidのプロセスのシャットダウンをシミュレートし、再びアプリが起動するときに何が起こるのかを検証します。
Note: 始める前に、API 28以上をサポートしているエミュレーターか実機で動作確認できることを確認してください。
ステップ1:プロセスシャットダウンをシミュレートするためにadbを利用する
Androidデバッグブリッジ(adb)はエミュレーターやコンピューターに接続された実機に命令を送ることができるコマンドラインツールです。このステップではadbを使ってアプリのプロセスを閉じ、Androidがアプリをシャットダウンしたときに何が起こるのかを検証します。
- コンパイルしてアプリを起動してください。カップケーキを何回かタップしてください。。
- ホームボタンをタップしてアプリをバックグラウンドにおいてください。現在アプリは停止した状態になり、Androidがこのアプリが使っているリソースを必要とした場合、閉じられるべき存在となりました。
- Android StudioでTerminalタブをクリックしてコマンドラインターミナルを開いてください。
- adbと入力してEnterを押してください。
Android Debug Bridge version X.XX.Xから始まり、tags to be used by logcat (see logcat -help)で終わる長文が表示された場合、正常です。
代わりにadb: command not foundが表示された場合、adbコマンドが実行パスで利用可能か確認してください。詳しくは、Utilities chapterの”Add adb to your execution path”を確認してください。 - 以下のコマンドをコマンドラインに貼り付けて実行してください。
adb shell am kill com.example.android.dessertclicker
このコマンドは接続された端末やエミュレータにdessertclickerというパッケージ名のプロセスを停止するように伝えるものです。ただしバックグラウンドにあるアプリに関してのみです。現在アプリはバックグラウンドにあるので、実機やエミュレータの画面にはプロセスが停止されたことを伝えるものはナインも表示されません。Android StudioのRunタブをクリックして、”Application terminated.”と表示されているメッセージを見てください。LogcatタブをクリックしてonDestroy()コールバックが実行されていないことを確認してください。アクティビティは単純に終了したのです。
- 最近開いた画面からアプリに戻ってください。アプリはバックグラウンドにある場合でも停止された場合でも最近開いた画面に表示されます。最近開いた画面からアプリに戻る際、アクティビティは再度スタートアップされます。アクティビティはonCreate()を含むスタートアップライフサイクルコールバックの一連のセットを実行します。
- アプリが再起動される際、スコア(desserts soldの数やトータル金額)がデフォルトの0にリセットされていることを確認してください。
Androidがアプリをシャッドダウンした場合、なぜ状態が保持されないのでしょうか。
OSによってアプリが再起動された場合、Androidはアプリが以前もっていた状態を復元しようと試みます。ユーザーがアクティビティから離れる際、Androidはいくつかのビューの状態を確認し、それをbundleに保存します。自動保存されるデータの例として、EditTextのテキスト(レイアウトでIDを持っている場合)や、アクティビティのバックスタックなどがあります。
しかしながら、Android OSは全てのデータを把握しているわけではありません。例として、DessertClickerアプリのrevenueのような独自の変数がある場合、Android OSにはこのデータやこのデータのアクティビティにおける重要性が分かりません。そのため、このデータは自身でbundleに追加する必要があります。
ステップ2:bundleにデータを保存するためにonSaveInstanceState()を使う
onSaveInstanceState()メソッドはAndroid OSがアプリを破棄した場合に保存したいデータがある場合に使うコールバックです。ライフサイクルコールバックダイアグラムの中では、onSaveInstanceState()はアクティビティが停止した後に呼び出されます。これはアプリがバックグラウンドに置かれる度に呼び出されます。

onSaveInstanceState()の呼び出しは安全対策として考えましょう。アクティビティがフォアグラウンドに出る度に少量の情報をbundleに保存します。システムがこのようにデータを保存するのは、仮にアプリをシャットダウンするまで待機した場合、OSにリソースの負荷がかかる可能性があるためです。毎回データを保存することで、必要に応じてバンドル内の更新データを復元できるようになります。
- MainActivityのonSaveInstanceState()コールバックをオーバーライドし、Timberログ文を追加してください。
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
Timber.i("onSaveInstanceState Called")
}
Note: onSaveInstanceState()のオーバーライドは二つあります。一つはoutStateパラメーターのみのもので、もう一つはoutStateとoutPersistentStateパラメーターがあるものです。上のコードで使われているoutStateのみの方を使ってください。
- コンパイルしてアプリを起動してください。ホームボンタンをタップしてアプリをバックグラウンドにおいてください。LogcatでonSaveInstanceState()コールバックがonPause()とonStop()の後に現れているのを確認してください。
- ファイルの先頭、クラス定義の直前に以下の定数を追加してください。
const val KEY_REVENUE = "revenue_key"
const val KEY_DESSERT_SOLD = "dessert_sold_key"
const val KEY_TIMER_SECONDS = "timer_seconds_key"
これらのキーはbundleにデータを保存するときと復元するときの両方で使用します。
- onSaveInstanceState()までスクロールしてください。Bunde型のoutStateパラメーターを確認してください。
bundleはキーと値がペアとなっているコレクションです。キーは必ずstringになります。値にはintやbooleanのようなプリミティブ型を入れることができます。
システムはこのbundleをRAMに保存しているため、bundle内のデータの容量は小さくするのがベストです。またそのサイズは端末によって異なりますが、bundleのサイズは制限されています。一般的に100k以内に収めるべきです。そうでない場合。TransactionTooLargeExceptionエラーによってアプリがクラッシュする危険性があります。 - onSaveInstanceState()の中で、revenueという値(integer)をputInt()メソッドによってbundleに入れてください。
outState.putInt(KEY_REVENUE, revenue)
putInt()メソッド(またBundleクラスの同じようなメソッドであるputFloat()とputStgring())は二つの引数を取ります。一つはキー用のstring(今回はKEY_REVENUE定数)で、もう一つは保存する値です。
- 同じ処理をDesserts soldの数と、タイマーの状態に対しても行ってください。
outState.putInt(KEY_DESSERT_SOLD, dessertsSold)
outState.putInt(KEY_TIMER_SECONDS, dessertTimer.secondsCount)
ステップ3:onCreate()からbundleデータを復元する
- onCreate()までスクロールしてください。
override fun onCreate(savedInstanceState: Bundle) {
onCreate()が呼びだされる度にBundleを取得していることを確認してください。アクティビティがプロセスのシャットダウンによって再起動されると、あなたが保存したbundleがonCreate()に渡されます。もしアクティビティがまっさらな状態で始まった場合、onCreate()のこのbundleがnullだったということです。ですので、bundleがnullでない場合、以前の保存された状態からアクティビティを再構成することになります。
Note: アクティビティが再生成されると、onRestoreInstanceState()コールバックがbundleと共にonStart()のあとに呼び出されます。ほとんどの場合、アクティビティの状態はonCreate()の中で復元します。しかしonRestoreInstanceState()はonStart()の後に呼び出されるので、もし何らかの状態をonCreate()の呼び出しのあとで復元したい場合は、onRestoreInstanceState()を使うこともできます。
- このコードをonCreate()のDessertTimerのセットアップのあとに追加してください。
if (savedInstanceState != null) {
revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
}
null検査によってbundleの中にデータが保存されているかどうかを確かめています。また bundleがnullだった場合、アプリがシャットダウンの後にフレッシュな状態で起動したか、再生成されたということになります。この検査はbundleからデータを復元する場合の一般的なパターンです。
ここで使ったキー(KEY_REVENUE)がputInt()の時に使ったキーと同じであることを確認してください。毎回同じキーを確実に使えるようにするために、キーは定数として定義しておくことが推奨されています。putInt()でデータをbundleに保存したのと同じように、getInt()でbundleからデータを取り出すことができます。getInt()には二つの引数があります。
- キーとなるstring。今回は”revenue_key”を表すKEY_REVENUE定数です。
- bundleに指定したキーに対する値が存在しない場合に返すデフォルトの値。今回の場合、KEY_REVENUEに対する値が事前に保存されていなかった場合、0が返されることになります。
bundleから取得した整数値はrevenue変数に代入されています。そしてUIがその値を利用します。
- デザートの数とタイマーの値を復元するためのgetInt()メソッドも追加します。
if (savedInstanceState != null) {
revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
dessertTimer.secondsCount =
savedInstanceState.getInt(KEY_TIMER_SECONDS, 0)
}
- コンパイルしてアプリを起動してください。カップケーキを最低5回はタップしてドーナツに変わるようにしてください。ホームボタンをタップしてアプリをバックグラウンドにおいてください。
- Android StudioのTeminalタブからアプリのプロセスをシャットダウンするために以下のコマンドを実行してください。
adb shell am kill com.example.android.dessertclicker
- 最近開いた画面からアプリに戻ってください。今回はアプリが正しい金額とデザートの数がbundleから復元された状態で戻っていることを確認してください。しかしデザートがカップケーキに戻っていることを確認してください。アプリがシャットダウンから戻ってきた後に完全に以前と同じ状態に戻るためには、もう一つしなければならないことがあります。
- MainActivityの中のshowCurrentDessert()メソッドを確認してください。このメソッドが現在のデザートの売れた数、およびallDesserts変数の中のデザートのリストに基づいて、アクティビティの中でどのデザートの画像を表示するかを決定しています。
for (dessert in allDesserts) {
if (dessertsSold >= dessert.startProductionAmount) {
newDessert = dessert
}
else break
}
このメソッドは正しい画像を選択するために売れたデザートの数を頼りにしています。従って、onSaveInstanceState()の中で、bundleに画像の参照を保存するために何かする必要はありません。bundleの中に既にデザートの売れた数を保存しています。
- onCreate()中、bundleから状態を復元するブロックの中でshowCurrentDessert()メソッドを呼び出してください。
if (savedInstanceState != null) {
revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
dessertTimer.secondsCount =
savedInstanceState.getInt(KEY_TIMER_SECONDS, 0)
showCurrentDessert()
}
- コンパイルしてアプリを起動してください。カップケーキをタップしてドーナツに変えた後にホームボタンを押してください。adbと使ってプロセスをシャットダウンしてください。最近開いた画面からアプリに戻ってください。値とデザートの画像が以前の状態に戻っていることを確認してください。