🐕

Android Kotlin Fundamentalsで学ぶ その4

2020/11/08に公開

はじめに

この記事はGoogleが提供しているCodelabの中のAndroidを作りながら学ぶAndroid Kotlin Fundamentalsコースで学習した内容を自分用に残していくものです。間違っていることなどあればコメントをいただけるとありがたいです!

この記事について

その4では、Lesson4について残していきます。
このレッスンでは、アクティビティフラグメントのライフサイクルと、複雑な状況の管理方法について説明しています。ライフサイクルイベントの確認のため、アプリにログを追加し、バグを修正、拡張機能の追加を行います。また、AndroidJetpackのライフサイクルライブラリによる簡単な保守・管理方法についても学びます。

4-1 Lifecycles and logging

ライフサイクルとは、アクティビティやフラグメントが初期化されてから破棄され、メモリが再利用されるまでの様々な状態。ある状態から別の状態に移行する時にコールバックを呼び出す。僕時のアクティビティでこれらのメソッドをオーバーライドして、ライフサイクルの状態の変化に応じてタスクを実行できる。

アクティビティのライフサイクル

Imgur(codelabsより)

アクティビティのステータスは図に示す通り。

  • 初期化状態: Initialized
  • onCreate後, onStop後: Created
  • onStart後, onPause後: Started
  • onResume後: Resumed
  • onDestroy: Destroyed

フラグメントのライフサイクル

Imgur(codelabsより)
フラグメントのライフサイクルはアクティビティと似ている

ライフサイクルのユースケース

Imgur
こんな感じにまとまるのかなと。

例えば、アクティビティを開始→ホームボタン→アプリに戻る→ホームボタン
こんな感じの動きをすると、メソッドの呼び出しはこんな感じになる
Imgur

4-2 Complex Lifecycle Situations

タイマーの設定

今回のプロジェクトにはDessertTimer.ktというタイマーを実装したクラスがあるのでそれを使う。
まずタイマーの設定

MainActivity.kt
    ...
    private lateinit var dessertTimer: DessertTimer
    override fun onCreate(...){
        ...
	dessertTimer = DessertTimer()
    }
    ...

ここで、MainActivityが呼び出された時(onStart())、表示されなくなった時(onStop())でタイマーを起動、停止する。

MainActivity.kt
    ...
    override fun onStart() {
        super.onStart()
        dessertTimer.startTimer()

        Timber.i("onStart Called")
    }

    override fun onStop() {
        super.onStop()
        dessertTimer.stopTimer()

        Timber.i("onStop Called")
    }
    ...

わかりやすく、アプリ起動後、一度ホームに戻った後、アプリ再開→ホームへ戻る操作をしたらこうなる。
Imgur
単純にonStartでタイマーが動き、onStopでタイマーが停止した。
ただ、複雑なアプリだと開始したけど停止し忘れてしまうこともある。こうなると、バグにつながる可能性がある。そこで、Jetpackライブラリ(ライフサイクルライブラリのひとつ)を使って簡素化できる。

ライフサイクルライブラリの特徴

  • ActivityFragmentはライフサイクルの所有者として扱われ、LifecycleOwnerインターフェースの実装をする
  • ライフサイクルクラスでは、現在の状態を保持し、変更が発生したらトリガーとなる。
  • ライフサイクルオブザーバとして、ライフサイクルの状態を監視し、変更時にタスクを実行する。LifecycleObserverインターフェースを実装

ライフサイクルライブラリに置き換え

DessertTimerLifecycleObserverにする。

DessertTimer.kt
// 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になってるみたい
だから、特に追記することなく、タイマーの呼び出しを行えばいい

MainActivity.ky

    override fun onCreate(savedInstanceState: Bundle?) {
	...
	// 呼び出しでライフサイクルを渡すだけ
        dessertTimer = DessertTimer(this.lifecycle)
	...
    }

これで、onStartonStopに特にタイマーを呼び出さなくてもよくなった。
ということは、タイマーを停止し忘れも気にしなくていいのかな?

コールバックでデータを保存

コールバックを使ってデータを保存する必要があるユースケースとその対象

  • アプリがフォアグラウンドからバックグラウンドに遷移した時に最後に表示していた画面に対して
  • アプリを終了する時に最後に表示していた画面に対して
  • 画面Aから画面Bに遷移する時、画面Aに対して
  • 画面が回転する時、回転前の画面に対して(構成の変更)
    onPauseonSaveInstanceStateの順で呼ばれ、onSaveInstanceState内でデータの保存処理を行う。
    (onSaveInstanceStateのタイミング参考)

保存はBundleへする。その際の注意点

  • {key, value}のコレクションで扱われること
  • システム内のRAMで保持されるため、保存するデータのサイズを小さくする必要があること(大きすぎるとエラーが発生する)
  • そのため、int型やboolean型を推奨

保存したい変数にキーを振る

MainActivity.kt
...
/** 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()でバンドルデータを保存する

MainActivity.kt
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()でバンドルデータの復元をする

MainActivity.kt
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