Android Kotlin Fundamentalsで学ぶ その4
はじめに
この記事はGoogleが提供しているCodelabの中のAndroidを作りながら学ぶAndroid Kotlin Fundamentalsコースで学習した内容を自分用に残していくものです。間違っていることなどあればコメントをいただけるとありがたいです!
この記事について
その4では、Lesson4について残していきます。
このレッスンでは、アクティビティ
とフラグメント
のライフサイクルと、複雑な状況の管理方法について説明しています。ライフサイクルイベントの確認のため、アプリにログを追加し、バグを修正、拡張機能の追加を行います。また、AndroidJetpackのライフサイクルライブラリによる簡単な保守・管理方法についても学びます。
Lifecycles and logging
4-1ライフサイクルとは、アクティビティやフラグメントが初期化されてから破棄され、メモリが再利用されるまでの様々な状態。ある状態から別の状態に移行する時にコールバックを呼び出す。僕時のアクティビティでこれらのメソッドをオーバーライドして、ライフサイクルの状態の変化に応じてタスクを実行できる。
アクティビティ
のライフサイクル
(codelabsより)
アクティビティのステータスは図に示す通り。
- 初期化状態: Initialized
- onCreate後, onStop後: Created
- onStart後, onPause後: Started
- onResume後: Resumed
- onDestroy: Destroyed
フラグメント
のライフサイクル
(codelabsより)
フラグメントのライフサイクルはアクティビティと似ている
ライフサイクルのユースケース
こんな感じにまとまるのかなと。
例えば、アクティビティを開始→ホームボタン→アプリに戻る→ホームボタン
こんな感じの動きをすると、メソッドの呼び出しはこんな感じになる
Complex Lifecycle Situations
4-2タイマーの設定
今回のプロジェクトにはDessertTimer.kt
というタイマーを実装したクラスがあるのでそれを使う。
まずタイマーの設定
...
private lateinit var dessertTimer: DessertTimer
override fun onCreate(...){
...
dessertTimer = DessertTimer()
}
...
ここで、MainActivity
が呼び出された時(onStart()
)、表示されなくなった時(onStop()
)でタイマーを起動、停止する。
...
override fun onStart() {
super.onStart()
dessertTimer.startTimer()
Timber.i("onStart Called")
}
override fun onStop() {
super.onStop()
dessertTimer.stopTimer()
Timber.i("onStop Called")
}
...
わかりやすく、アプリ起動後、一度ホームに戻った後、アプリ再開→ホームへ戻る
操作をしたらこうなる。
単純にonStart
でタイマーが動き、onStop
でタイマーが停止した。
ただ、複雑なアプリだと開始したけど停止し忘れてしまうこともある。こうなると、バグにつながる可能性がある。そこで、Jetpack
ライブラリ(ライフサイクルライブラリのひとつ)を使って簡素化できる。
ライフサイクルライブラリ
の特徴
-
Activity
やFragment
はライフサイクルの所有者として扱われ、LifecycleOwner
インターフェースの実装をする - ライフサイクルクラスでは、現在の状態を保持し、変更が発生したらトリガーとなる。
- ライフサイクルオブザーバとして、ライフサイクルの状態を監視し、変更時にタスクを実行する。
LifecycleObserver
インターフェースを実装
ライフサイクルライブラリに置き換え
DessertTimer
をLifecycleObserver
にする。
// LifecycleObserverをインプリメント
class DessertTimer(lifecycle: Lifecycle): LifecycleObserver {
...
@OnLifecycleEvent(Lifecycle.Event.ON_START) // アノテーションを付与
fun startTimer() {
...
}
init {
lifecycle.addObserver(this)
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP) // アノテーションを付与
fun stopTimer() {
...
}
}
MainActivity
は、AppcompatActivity
の継承によってすでにLifecycleOwner
になってるみたい
だから、特に追記することなく、タイマーの呼び出しを行えばいい
override fun onCreate(savedInstanceState: Bundle?) {
...
// 呼び出しでライフサイクルを渡すだけ
dessertTimer = DessertTimer(this.lifecycle)
...
}
これで、onStart
、onStop
に特にタイマーを呼び出さなくてもよくなった。
ということは、タイマーを停止し忘れも気にしなくていいのかな?
コールバックでデータを保存
コールバックを使ってデータを保存する必要があるユースケースとその対象
- アプリがフォアグラウンドからバックグラウンドに遷移した時に最後に表示していた画面に対して
- アプリを終了する時に最後に表示していた画面に対して
- 画面Aから画面Bに遷移する時、画面Aに対して
- 画面が回転する時、回転前の画面に対して(構成の変更)
onPause
→onSaveInstanceState
の順で呼ばれ、onSaveInstanceState
内でデータの保存処理を行う。
(onSaveInstanceStateのタイミング参考)
保存はBundle
へする。その際の注意点
- {key, value}のコレクションで扱われること
- システム内のRAMで保持されるため、保存するデータのサイズを小さくする必要があること(大きすぎるとエラーが発生する)
- そのため、
int
型やboolean
型を推奨
保存したい変数にキーを振る
...
/** onSaveInstanceState Bundle Keys **/
const val KEY_REVENUE = "revenue_key"
const val KEY_DESSERT_SOLD = "dessert_sold_key"
const val KEY_TIMER_SECONDS = "timer_seconds_key"
class MainActivity : AppCompatActivity() {
...
}
今回は3つの変数を保存したいので、3つのキーを設定
onSaveInstanceState()でバンドルデータを保存する
override fun onSaveInstanceState(outState: Bundle){
super.onSaveInstanceState(outState)
outState.putInt(KEY_REVENUE, revenue)
outState.putInt(KEY_DESSERT_SOLD, dessertsSold)
outState.putInt(KEY_TIMER_SECONDS, dessertTimer.secondsCount)
}
このように、outState.put**
メソッドを用いてキーと値を紐付ける
onCreate()でバンドルデータの復元をする
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
// 復元処理
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()
}
// Set the TextViews to the right values
binding.revenue = revenue
binding.amountSold = dessertsSold
// Make sure the correct dessert is showing
binding.dessertButton.setImageResource(currentDessert.imageId)
}
復元では、savedInstanceState.get**()
メソッドでバンドルからデータを取得する。この時、バンドル内に第一引数のキーの値があればそれを、なければ第二引数の値を返すようになる。
また、バンドルからデータを取得する際、
if (savedInstanceState != null) {
...
}
このようにif文で復元処理を始める理由
-
savedInstanceState != null
のとき、何らかの理由でアクティビティが停止し、再開(復元)する必要がある -
savedInstanceState == null
のとき、アクティビティが新しく始まったため、復元する必要がない
からである。
他にも、onRestoreInstanceState()
でも復元処理が可能だったり、EditText
など一部のコンテンツは自動で保存されたりするらしい。
復元処理の場所について
上で書いたように、復元処理はonCreate()
か、onRestoreInstanceState()
で行うのだが、その使い分けやタイミングに違いがあるみたい。
onCreate()
は、アクティビティを新しく作成する場合、再構築する場合の両方で呼び出される。そのため、新規作成 or 再構築のどちらかを判断するために、if文でインスタンスでnullチェックする必要があった。
ただ、onRestoreInstanceState()
はonStart()
メソッドメソッドの直後に呼ばれるため、呼ばれるタイミングが再構築の場合のみ。なので、再構築に必要な処理を記述するだけで良いため、if文でインスタンスのnullチェックはいらない。
まとめ
今回は主にライフサイクルと各状態での処理について学んだ。ライフサイクルをしっかり理解しないと、必要なデータを残せなかったり、クラッシュしてしまったりとユーザに不便なもの(脆弱)になってしまう。そのため、このセクションの内容は十分理解したいと思いました。画面の向きを変えるだけでもアクティビティの再構築が行われることを知らなかったので、とても驚いたというかしれてよかったなと思いました。
Discussion