アクティビティのライフサイクルのステージ

1. 始める前に

この Codelab では、Android の基本的な要素であるアクティビティのライフサイクルについて学習します。

アクティビティは、その存続期間にわたってさまざまな状態に遷移し、場合によっては元の状態に戻ります。この状態の遷移を、アクティビティのライフサイクルといいます。

Android では、アクティビティはユーザーとやり取りするためのエントリ ポイントです。

これまでは、1 つのアクティビティでアプリの画面を 1 つ表示していました。現在のベスト プラクティスでは、1 つのアクティビティで必要に応じて画面を切り替えて複数の画面を表示することがあります。

アクティビティのライフサイクルは、アクティビティの作成から破棄(システムがそのアクティビティのリソースを再利用する)まで続きます。ユーザーがアクティビティを出入りすると、各アクティビティはアクティビティのライフサイクルの状態間を遷移します。

Android デベロッパーは、アクティビティのライフサイクルを理解しておく必要があります。アクティビティがライフサイクルの状態の変化に正しく反応しない場合、アプリに予期せぬバグが発生してユーザーの混乱を招いたり、Android システムのリソースを過度に使用したりする可能性があります。Android のライフサイクルを理解し、ライフサイクルの状態の変化に適切に対応することは、Android の開発における重要な部分です。

前提条件

  • アクティビティの概要と、アプリでアクティビティを作成する方法に関する知識
  • アクティビティの onCreate() メソッドの概要と、このメソッドで行われるオペレーションの種類に関する知識

学習内容

  • ロギング情報を Logcat に出力する方法
  • Activity のライフサイクルの基本と、アクティビティが状態間を遷移したときに呼び出されるコールバック
  • ライフサイクル コールバック メソッドをオーバーライドして、アクティビティのライフサイクルのさまざまなタイミングでオペレーションを行う方法

作成するアプリの概要

  • Dessert Clicker というスターター アプリを変更して、Logcat に表示されるロギング情報を追加します。
  • ライフサイクル コールバック メソッドをオーバーライドして、アクティビティの状態の変化を記録します。
  • アプリを実行し、アクティビティの開始、停止、再開時に表示されるロギング情報をメモします。
  • rememberSaveable を実装して、デバイス構成が変更された場合に失われる可能性があるアプリデータを保持します。

2. アプリの概要

この Codelab では、Dessert Clicker というスターター アプリを使用します。Dessert Clicker では、ユーザーが画面上のデザートをタップするたびに、そのデザートが「購入」されます。アプリは以下について、レイアウトの値を更新します。

  • 「購入された」デザートの数
  • 「購入済み」のデザートの総収益

245d0bdfc09f4d54.png

このアプリには、Android のライフサイクルに関連するバグがいくつか含まれています。たとえば、特定の状況でデザートの値が 0 にリセットされます。Android のライフサイクルを理解することで、このような問題が発生する理由とその解決方法がわかるようになります。

スターター コードをダウンロードする

Android Studio で basic-android-kotlin-compose-training-dessert-clicker フォルダを開きます。

3.ライフサイクル メソッドを確認し、基本的なロギングを追加する

どのアクティビティにもライフサイクルというものがあります。これは植物や動物のライフサイクルになぞらえた用語であり、たとえば蝶のライフサイクルでは、卵から幼虫、さなぎ、蝶になり、死に至るまでの過程で、さまざまな状態を遷移します。

蝶のライフサイクル - 卵から幼虫、さなぎ、蝶になり、死に至る。

アクティビティのライフサイクルも同様に、最初の初期化から、オペレーティング システム(OS)によってメモリが回収される破棄までの過程で、アクティビティが遷移するさまざまな状態によって構成されています。通常、プログラムのエントリ ポイントは main() メソッドです。しかし Android のアクティビティは onCreate() メソッドで始まります。このメソッドは、上記の例でいうと卵の段階に相当します。このコースではすでにアクティビティを何度か使用しているため、onCreate() メソッドをご存じかもしれません。ユーザーがアプリの起動、アクティビティ間の移動、アプリ内外への移動を行うと、アクティビティの状態が変わります。

次の図は、アクティビティのライフサイクルの状態をすべて示しています。これらの状態は、名前が示すようにそれぞれアクティビティのステータスを表します。蝶のライフサイクルとは異なり、アクティビティは一方向にしか移動しないのではなく、ライフサイクル全体で状態間を行き来できます。

ca808edb1c95f07a.png

多くの場合、アクティビティのライフサイクルの状態が変わったタイミングで、動作の変更やコードの実行が必要になります。したがって、Activity クラス自体と、Activity のサブクラス(ComponentActivity など)は、一連のライフサイクル コールバック メソッドを実装しています。Android は、アクティビティがある状態から別の状態に遷移したときに、これらのコールバックを呼び出します。そのため、ライフサイクルの状態の変化に応じて、自分のアクティビティ内でこれらのメソッドをオーバーライドしてタスクを実行できます。次の図は、ライフサイクルの状態と、オーバーライド可能なコールバックを示しています。

アクティビティのライフサイクルのスキーム

Android がオーバーライド可能なコールバックを呼び出すタイミングと、各コールバック メソッドで行われる処理を理解することは重要ですが、どちらの図も複雑でわかりにくくなっています。この Codelab では、それぞれの状態とコールバックの概要に触れるだけでなく、追究して Android アクティビティのライフサイクルについて理解を深めます。

ステップ 1: onCreate() メソッドを確認してロギングを追加する

Android のライフサイクルで何が起きているのかを調べる際に、さまざまなライフサイクル メソッドが呼び出されるタイミングを把握しておくと役に立ちます。この情報により、Dessert Clicker アプリのどの部分に問題があるのかを特定できます。

この情報は、Android のロギング機能を使用して簡単に特定できます。ロギングを使用すると、アプリの実行中にコンソールに短いメッセージを書き込むことができます。これを使用して、各コールバックがトリガーされたときにメッセージを表示できます。

  1. Dessert Clicker アプリを実行し、デザートの写真を複数回タップします。[Desserts sold] の値と合計金額がどのように変化するかに注目してください。
  2. MainActivity.kt を開き、このアクティビティの onCreate() メソッドを確認します。
override fun onCreate(savedInstanceState: Bundle?) {
    // ...
}

アクティビティのライフサイクルの図にある onCreate() メソッドに見覚えがあるかもしれません。以前にこのコールバックを使用したことがあるはずです。このメソッドはすべてのアクティビティに実装する必要があります。onCreate() メソッドでは、アクティビティに対して 1 回限りの初期化を行います。たとえば onCreate() では、setContent() を呼び出してアクティビティの UI レイアウトを指定します。

onCreate ライフサイクル メソッド

onCreate() ライフサイクル メソッドは、アクティビティの初期化直後(OS によって新しい Activity オブジェクトがメモリに作成されたとき)に 1 回呼び出されます。onCreate() が実行されると、アクティビティは作成されたと見なされます。

  1. 次の定数を、MainActivity.kt のトップレベル(クラス宣言 class MainActivity の上)に追加します。

値が変更されないため、ファイルで TAG 定数を宣言することをおすすめします。

コンパイル時の定数としてマークするには、変数を宣言するときに const を使用します。コンパイル時の定数は、コンパイル時に判明する値です。

private const val TAG = "MainActivity"
  1. onCreate() メソッドで、super.onCreate() 呼び出しの直後に次の行を追加します。
Log.d(TAG, "onCreate Called")
  1. 必要に応じて Log クラスをインポートします(Alt+Enter(Mac の場合は Option+Enter)を押して、[Import] を選択します)。自動インポートを有効にしている場合、インポートは自動的に行われます。
import android.util.Log

Log クラスは、メッセージを Logcat に書き込みます。Logcat は、メッセージを記録するためのコンソールです。Android からのアプリに関するメッセージ(Log.d() メソッドや他の Log クラスメソッドを使用してログに明示的に送信したメッセージを含む)がここに表示されます。

Log 命令には、重要な部分が 3 つあります。

  • ログメッセージの優先度(メッセージの重要度)。この例では、Log.v() は詳細メッセージをログに記録します。Log.d() メソッドはデバッグ メッセージを書き込みます。Log クラスのその他のメソッドには、情報メッセージ用の Log.i()、警告メッセージ用の Log.w()、エラー メッセージ用の Log.e() が含まれます。
  • ログの tag(最初のパラメータ)。この例では "MainActivity"。このタグは、Logcat でログメッセージを簡単に見つけるための文字列です。通常、このタグはクラスの名前です。
  • 実際のログメッセージ msg(2 番目のパラメータ)。短い文字列であり、この例では "onCreate Called"

a4ff4aa74384ff6.png

  1. Dessert Clicker アプリをコンパイルして実行します。デザートをタップしても、アプリの動作には特に違いは生じません。Android Studio の画面の下部で [Logcat] タブをクリックします。

ed03d4bb1f020995.png

  1. [Logcat] ウィンドウで、検索フィールドに「tag:MainActivity」と入力します。

961ea44c4b9ee3c.png

Logcat には多数のメッセージが含まれている場合がありますが、ほとんどは役に立ちません。Logcat エントリはさまざまな方法でフィルタできますが、検索するのが最も簡単です。コード内で MainActivity をログタグとして使用したため、そのタグを使用してログをフィルタできます。ログメッセージには、日時、ログタグ、パッケージの名前(com.example.dessertclicker)、実際のメッセージが含まれます。このメッセージがログに表示されていることから、onCreate() が実行されたことがわかります。

ステップ 2: onStart() メソッドを実装する

onStart() ライフサイクル メソッドは、onCreate() の直後に呼び出されます。onStart() が実行されると、アクティビティが画面に表示されます。アクティビティを初期化するために 1 回だけ呼び出される onCreate() とは異なり、onStart() はアクティビティのライフサイクルでシステムから何度も呼び出すことができます。

a357d2291de472d9.png

onStart() は、対応する onStop() ライフサイクル メソッドとペアになることに注意してください。ユーザーがアプリを起動してからデバイスのホーム画面に戻ると、アクティビティは停止し、画面に表示されなくなります。

  1. Android Studio で、MainActivity.kt を開き、MainActivity クラス内にカーソルを置いて、[Code] > [Override Methods...] を選択するか、Control+O を押します。ダイアログが開き、このクラスでオーバーライドできるすべてのメソッドの長いリストが表示されます。

11ff93bee1c3940f.png

  1. onStart を入力して、適切なメソッドを検索します。次の一致項目までスクロールするには、下矢印を使用します。リストから「onStart()」を選択し、[OK] をクリックしてボイラープレート オーバーライド コードを挿入します。コードは次の例のようになります。
override fun onStart() {
    super.onStart()
}
  1. onStart() メソッド内に、ログメッセージを追加します。
override fun onStart() {
    super.onStart()
    Log.d(TAG, "onStart Called")
}
  1. Dessert Clicker アプリをコンパイルして実行し、[Logcat] ペインを開きます。
  2. 検索フィールドに「tag:MainActivity」と入力して、ログをフィルタします。onCreate() メソッドと onStart() メソッドの両方が相次いで呼び出され、アクティビティが画面に表示されます。
  3. デバイスのホームボタンを押してから、履歴画面を使用してアクティビティに戻ります。アクティビティは、中断したところからすべて同じ値で再開され、2 回目の onStart() が Logcat に記録されます。また、onCreate() メソッドが再度呼び出されることはありません。
2024-02-20 10:30:00.231  5684-5684  MainActivity            com.example.dessertclicker           D  onCreate Called
2024-02-20 10:30:00.278  5684-5684  MainActivity            com.example.dessertclicker           D  onStart Called
2024-02-20 10:30:39.020  5684-5684  MainActivity            com.example.dessertclicker           D  onStart Called

ステップ 3: ログ ステートメントを追加する

このステップでは、他のすべてのライフサイクル メソッドのロギングを実装します。

  1. 次のコードに示すように、MainActivity の残りのライフサイクル メソッドをオーバーライドして、それぞれのログ ステートメントを追加します。
override fun onResume() {
    super.onResume()
    Log.d(TAG, "onResume Called")
}

override fun onRestart() {
    super.onRestart()
    Log.d(TAG, "onRestart Called")
}

override fun onPause() {
    super.onPause()
    Log.d(TAG, "onPause Called")
}

override fun onStop() {
    super.onStop()
    Log.d(TAG, "onStop Called")
}

override fun onDestroy() {
    super.onDestroy()
    Log.d(TAG, "onDestroy Called")
}
  1. Dessert Clicker を再度コンパイルして実行し、Logcat を確認します。

今回は onCreate()onStart() に加えて、onResume() ライフサイクル コールバックのログメッセージが表示されています。

2024-02-20 10:33:36.033  5789-5789  MainActivity            com.example.dessertclicker           D  onCreate Called
2024-02-20 10:33:36.073  5789-5789  MainActivity            com.example.dessertclicker           D  onStart Called
2024-02-20 10:33:36.075  5789-5789  MainActivity            com.example.dessertclicker           D  onResume Called

アクティビティが最初から開始されると、次の 3 つのライフサイクル コールバックが順に呼び出されます。

  • onCreate(): システムがアプリを作成するとき。
  • onStart(): アプリが画面に表示されますが、ユーザーはまだ操作できません。
  • onResume(): アプリがフォアグラウンドになり、ユーザーが操作できるようになります。

onResume() メソッドは、その名前とは異なり、再開する対象がない場合でも起動時に呼び出されます。

アクティビティのライフサイクルのスキーム

4. ライフサイクルのユースケースを確認する

Dessert Clicker アプリをロギング用にセットアップできたところで、アプリの使用を開始して、ライフサイクル コールバックがどのようにトリガーされるのかを確認しましょう。

ユースケース 1: アクティビティの開始と終了

アプリを初めて起動して終了するという、最も基本的なユースケースから始めます。

  1. Dessert Clicker アプリをまだ実行していない場合は、コンパイルして実行します。すでに説明したとおり、アクティビティの初回起動時に onCreate()onStart()onResume() のコールバックが呼び出されます。
2024-02-20 10:33:36.033  5789-5789  MainActivity            com.example.dessertclicker           D  onCreate Called
2024-02-20 10:33:36.073  5789-5789  MainActivity            com.example.dessertclicker           D  onStart Called
2024-02-20 10:33:36.075  5789-5789  MainActivity            com.example.dessertclicker           D  onResume Called
  1. カップケーキを数回タップします。
  2. デバイスの戻るボタンをタップします。

onPause()onStop() の順序で呼び出されていることが、Logcat で確認できます。

2024-02-20 10:34:41.974  5789-5789  MainActivity            com.example.dessertclicker           D  onPause Called
2024-02-20 10:34:42.411  5789-5789  MainActivity            com.example.dessertclicker           D  onStop Called

この場合、戻るボタンを使用することで、アクティビティ(とアプリ)が画面に表示されなくなり、バックスタックに移動します。

Android OS は、コードがアクティビティの finish() メソッドを呼び出すか、ユーザーがアプリを強制終了した場合に、アクティビティを終了することがあります。たとえばユーザーは、履歴画面でアプリを強制終了または終了できます。アプリが長時間画面に表示されていない場合、OS が独自にアクティビティをシャットダウンすることもあります。これにより、Android はバッテリー消費を抑え、アプリが使用していたリソースを回収して他のアプリで使用できるようにします。これらは、Android システムがアクティビティを破棄する理由のほんの一部です。Android システムが警告を表示せずにアクティビティを破棄するケースもあります。

ユースケース 2: アクティビティ間の移動

アプリを起動して終了したことで、アクティビティが初めて作成されたときのライフサイクルの状態について、大部分を確認できました。また、アクティビティが終了されたときのライフサイクルの状態についても、大部分を確認しました。しかし、ユーザーは Android デバイスを操作する際に、アプリを切り替えたり、ホームに戻ったり、新しいアプリを起動したり、電話などの他のアクティビティによる中断を処理したりします。

その場合、ユーザーがアクティビティから離れるたびにそのアクティビティが完全に終了するわけではありません。

  • アクティビティが画面に表示されなくなると、そのアクティビティはバックグラウンドに移行します(これとは逆の状態が、アクティビティがフォアグラウンドにあるとき、または画面に表示されているときです)。
  • ユーザーがアプリに戻ると、同じアクティビティが再開され、再び表示されます。ライフサイクルのこの部分は、アプリの表示可能なライフサイクルと呼ばれます。

一般に、アプリがバックグラウンドにあるときは、システム リソースとバッテリーの消耗を抑えるために、アプリがアクティブに実行されないようにする必要があります。Activity ライフサイクルとそのコールバックを使用して、アプリがバックグラウンドに移行するタイミングを把握することで、実行中のオペレーションを一時停止できます。その後、アプリがフォアグラウンドになったらオペレーションを再開します。

このステップでは、アプリがバックグラウンドに移行してフォアグラウンドに戻ったときのアクティビティのライフサイクルを確認します。

  1. Dessert Clicker アプリを実行した状態で、カップケーキを数回クリックします。
  2. デバイスのホームボタンを押して、Android Studio の Logcat を確認します。ホーム画面に戻ってもアプリは完全にシャットダウンされることはなく、バックグラウンドに移行されます。onPause() メソッドと onStop() メソッドが呼び出されています。
2024-02-20 10:35:26.832  5789-5789  MainActivity            com.example.dessertclicker           D  onPause Called
2024-02-20 10:35:27.233  5789-5789  MainActivity            com.example.dessertclicker           D  onStop Called

onPause() が呼び出されると、アプリのフォーカスが失われます。onStop() の後、アプリは画面に表示されなくなります。アクティビティは停止されていますが、Activity オブジェクトはメモリ内(バックグラウンド)に残っています。Android OS がアクティビティを破棄したわけではありません。ユーザーがアプリに戻ってくる可能性があるため、Android はアクティビティ リソースを保持しています。

c470ee28ab7f8a1a.png

  1. 履歴画面を使用してアプリに戻ります。エミュレータでは、次の図に示す正方形のシステムボタンから履歴画面にアクセスできます。

Logcat を確認すると、アクティビティが onRestart()onStart() で再起動し、その後 onResume() で再開しています。

bc156252d977e5ae.png

2024-02-20 10:36:05.837  5789-5789  MainActivity            com.example.dessertclicker           D  onRestart Called
2024-02-20 10:36:05.839  5789-5789  MainActivity            com.example.dessertclicker           D  onStart Called
2024-02-20 10:36:05.842  5789-5789  MainActivity            com.example.dessertclicker           D  onResume Called

アクティビティがフォアグラウンドに戻ったときに、onCreate() メソッドが再び呼び出されることはありません。アクティビティ オブジェクトは破棄されていないため再作成の必要はなく、onCreate() の代わりに onRestart() メソッドが呼び出されます。アクティビティがフォアグラウンドに戻ったときに、[Desserts sold] の数値が保持されていることに注意してください。

  1. デバイスの履歴画面に複数のアプリが表示されるように、Dessert Clicker 以外のアプリを少なくとも 1 つ起動します。
  2. 履歴画面を開き、別の最近のアクティビティを開きます。最近使ったアプリに戻って、Dessert Clicker をフォアグラウンドに戻します。

ホームボタンを押したときと同じコールバックが Logcat に表示されていることに注意してください。アプリがバックグラウンドになるときに onPause()onStop() が呼び出され、フォアグラウンドに戻るときに onRestart()onStart()onResume() が呼び出されます。

これらのメソッドは、アプリが停止してバックグラウンドに移行するとき、またはアプリが再開されてフォアグラウンドに戻るときに呼び出されます。この間にアプリでなんらかの処理を行う必要がある場合は、関連するライフサイクル コールバック メソッドをオーバーライドします。

ユースケース 3: アクティビティの一部を非表示にする

アプリが起動されて onStart() が呼び出されると、アプリが画面に表示されることを説明しました。onResume() が呼び出されると、アプリはユーザー フォーカスを取得します。つまり、ユーザーはアプリを操作できます。ライフサイクルのうち、アプリが完全に画面に表示され、ユーザー フォーカスがある期間を、フォアグラウンドの存続期間といいます。

アプリがバックグラウンドに移行すると、onPause() の後にフォーカスが失われ、onStop() 以降はアプリが表示されなくなります。

フォーカスと表示の違いは重要です。アクティビティは、ユーザー フォーカスがなくても画面上に部分的に表示できます。このステップでは、アクティビティが部分的に表示されているものの、ユーザー フォーカスがないケースを見ていきます。

  1. Dessert Clicker アプリが実行されている状態で、画面右上の共有ボタンをクリックします。

共有アクティビティが画面の下半分に表示されますが、アクティビティは引き続き画面の上半分に表示されています。

677c190d94e57447.pngca6285cbbe3801cf.png

  1. Logcat を確認すると、onPause() のみが呼び出されています。
2024-02-20 10:36:42.886  5789-5789  MainActivity            com.example.dessertclicker           D  onPause Called

このユースケースでは、アクティビティがまだ部分的に表示されているため、onStop() は呼び出されません。ただし、ユーザー フォーカスはこのアクティビティではなく、フォアグラウンドの「共有」アクティビティにあるため、ユーザーはこのアクティビティを操作できません。

ここで、フォーカスと表示の違いが重要な理由を考えてみましょう。onPause() のみによる中断は通常、そのアクティビティに戻るか、別のアクティビティまたはアプリに移動するまでの短い時間しか持続しません。一般に、UI を継続的に更新して、アプリの残りの部分がフリーズしたように見えないようにする必要があります。

onPause() で実行されるコードにより、他のアクティビティやアプリの表示が妨げられるため、onPause() のコードは軽量化します。たとえば、電話の着信があった場合に、onPause() のコードによって着信通知が遅れる可能性があります。

  1. 共有ダイアログの外側をクリックするとアプリに戻り、onResume() が呼び出されます。

onResume()onPause() はどちらもフォーカスに関係があります。onResume() メソッドはアクティビティがフォーカスを得たときに呼び出され、onPause() はアクティビティがフォーカスを失ったときに呼び出されます。

5. 構成の変更について確認する

また、アクティビティのライフサイクルを管理するうえで、構成の変更がアクティビティのライフサイクルにどのように影響するかを理解しておくことも重要です。

構成の変更は、デバイスの状態が大きく変更されたために、システムにとってアクティビティを完全にシャットダウンして再構築するのが最も手間がかからない場合に行われます。たとえば、ユーザーがデバイスの言語を変更した場合、異なるテキスト方向と文字列の長さに対応するためにレイアウト全体の変更が必要になる可能性あります。ユーザーがデバイスをホルダーに装着した場合や物理キーボードを追加した場合は、アプリのレイアウトで別のディスプレイ サイズやレイアウトを利用する必要があるかもしれません。また、デバイスの向きが変わった場合(デバイスが縦向きから横向き、またはその逆に回転した場合)は、新しい向きに合わせてレイアウトの変更が必要になることがあります。次のシナリオで、アプリがどのように動作するか見てみましょう。

最後に紹介するライフサイクル コールバックは、onStop() の後に呼び出される onDestroy() です。これは、アクティビティが破棄される直前に呼び出されます。アプリのコードが finish() を呼び出すとき、または構成の変更によりシステムがアクティビティを破棄して再作成する必要があるときに発生する可能性があります。

構成の変更により onDestroy() が呼び出される

画面の回転は構成の変更の一種であり、アクティビティのシャットダウンと再開が発生します。この構成の変更をシミュレートして影響を調べるには、次の手順を実施します。

  1. アプリをコンパイルして実行します。
  2. エミュレータでは、画面の回転のロックが無効になっていることを確認してください。
  3. デバイスまたはエミュレータを回転させて横向きにします。エミュレータを左右に回転させるには、回転ボタンを使用します。
  4. Logcat を調べて、アクティビティがシャットダウンするときに、onPause()onStop()onDestroy() をこの順序で呼び出すことを確認します。
2024-02-20 10:37:57.078  5987-5987  MainActivity            com.example.dessertclicker           D  onPause Called
2024-02-20 10:37:57.087  5987-5987  MainActivity            com.example.dessertclicker           D  onStop Called
2024-02-20 10:37:57.102  5987-5987  MainActivity            com.example.dessertclicker           D  onDestroy Called

デバイスの回転時のデータ損失

  1. アプリをコンパイルして実行し、Logcat を開きます。
  2. カップケーキを数回クリックすると、デザートが販売され、総収益は 0 でなくなります。
  3. エミュレータでは、画面の回転のロックが無効になっていることを確認してください。
  4. デバイスまたはエミュレータを回転させて横向きにします。エミュレータを左右に回転させるには、回転ボタンを使用します。

f745d0e2697415fd.png

  1. Logcat で出力を調べます。MainActivity の出力をフィルタします。
2024-02-20 10:39:22.724  6087-6087  MainActivity            com.example.dessertclicker           D  onCreate Called
2024-02-20 10:39:22.752  6087-6087  MainActivity            com.example.dessertclicker           D  onStart Called
2024-02-20 10:39:22.753  6087-6087  MainActivity            com.example.dessertclicker           D  onResume Called
2024-02-20 10:39:40.508  6087-6087  MainActivity            com.example.dessertclicker           D  onPause Called
2024-02-20 10:39:40.540  6087-6087  MainActivity            com.example.dessertclicker           D  onStop Called
2024-02-20 10:39:40.549  6087-6087  MainActivity            com.example.dessertclicker           D  onDestroy Called
2024-02-20 10:39:40.582  6087-6087  MainActivity            com.example.dessertclicker           D  onCreate Called
2024-02-20 10:39:40.584  6087-6087  MainActivity            com.example.dessertclicker           D  onStart Called
2024-02-20 10:39:40.585  6087-6087  MainActivity            com.example.dessertclicker           D  onResume Called

デバイスまたはエミュレータの画面を回転させると、すべてのライフサイクル コールバックが呼び出され、アクティビティがシャットダウンされます。その後アクティビティが再作成されると、すべてのライフサイクル コールバックが呼び出され、アクティビティが開始されます。

デバイスが回転し、アクティビティがシャットダウンされて再作成されると、アクティビティはデフォルト値で再開します。つまり、デザートの販売数と総収益がゼロにリセットされます。

これらの値がリセットされる理由と修正方法を知るには、コンポーザブルのライフサイクルと、それが状態の監視と保持をどのように把握しているのかを知る必要があります。

コンポーザブルのライフサイクル

アプリの UI は、最初は Composition というプロセスでコンポーズ可能な関数を実行することで構築されます。

アプリの状態が変化すると、再コンポーズがスケジュール設定されます。再コンポーズとは、状態が変化した可能性のあるコンポーズ可能な関数を Compose が再実行し、更新された UI を作成することです。Composition は、こうした変化を反映するように更新されます。

Composition は、初回のコンポーズとその後の再コンポーズによってのみ作成、更新できます。

コンポーズ可能な関数には、アクティビティのライフサイクルとは独立した、独自のライフサイクルがあります。そのライフサイクルは、「Composition に入る」、「0 回以上再コンポーズする」、「Composition から出る」というイベントで構成されます。

Compose が再コンポーズをトラッキングし、トリガーするには、状態がいつ変化したのかを把握する必要があります。オブジェクトの状態をトラッキングするよう Compose に指示するには、オブジェクトの型を State または MutableState にする必要があります。State 型は不変であり、読み取りのみが可能です。MutableState 型は可変であり、読み取りと書き込みが可能です。

MutableState については、以前の Codelab の Lemonade アプリTip Time アプリですでに確認、使用しました。

可変変数 revenue を作成するには、mutableStateOf を使用して宣言します。0 は初期のデフォルト値です。

var revenue = mutableStateOf(0)

これは、収益値が変更されたときに Compose で再コンポーズをトリガーするには十分ですが、更新された値を保持するには不十分です。コンポーザブルが再実行されるたびに、収益値が再初期化されて初期のデフォルト値である 0 になります。

再コンポーズの際にその値を保持および再利用するよう Compose に指示するには、remember API で宣言する必要があります。

var revenue by remember { mutableStateOf(0) }

revenue の値が変更されると、Compose は、この値を読み取るすべてのコンポーズ可能な関数を、再コンポーズのためにスケジュール設定します。

Compose は再コンポーズ時に収益の状態を記憶しますが、構成の変更の際はこの状態を保持しません。構成の変更の際に Compose が状態を保持するようにするには、rememberSaveable を使用する必要があります。

その他の演習と詳細については、Compose の状態の概要 Codelab をご覧ください。

rememberSaveable を使用して構成の変更をまたいで値を保存する

Android OS がアクティビティを破棄して再作成する場合に必要な値を保存するには、rememberSaveable 関数を使用します。

再コンポーズの際に値を保存するには、remember を使用する必要があります。再コンポーズ時と構成の変更時の両方で値を保存するには、rememberSaveable を使用します。

rememberSaveable を使用して値を保存すると、アクティビティが復元されたときに必要に応じて使用できるようになります。

  1. MainActivity で、現在 remember を使用している 5 つの変数のグループを rememberSaveable に更新します。
var revenue by remember { mutableStateOf(0) }
...
var currentDessertImageId by remember {
    mutableStateOf(desserts[currentDessertIndex].imageId)
}
var revenue by rememberSaveable { mutableStateOf(0) }
...
var currentDessertImageId by rememberSaveable {
    mutableStateOf(desserts[currentDessertIndex].imageId)
}
  1. アプリをコンパイルして実行します。
  2. カップケーキを数回クリックすると、デザートが販売され、総収益は 0 でなくなります。
  3. デバイスまたはエミュレータを回転させて横向きにします。
  4. アクティビティを破棄して再作成すると、デザートの画像、デザートの販売数、総収益が以前の値に戻ることを確認します。

6. 解答コード

7. まとめ

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

  • アクティビティのライフサイクルは、アクティビティが移行する一連の状態です。アクティビティのライフサイクルは、Android OS が最初にアクティビティを作成したときに開始し、OS がアクティビティを破棄したときに終了します。
  • ユーザーがアクティビティ間やアプリの内外に移動すると、各アクティビティはライフサイクルの状態間を移行します。
  • アクティビティのライフサイクルの各状態には、Activity クラスでオーバーライド可能な、対応するコールバック メソッドがあります。ライフサイクル メソッドのコアセットは、onCreate()onRestart()onStart()onResume()onPause()onStop()onDestroy() です。
  • アクティビティがあるライフサイクル状態に移行したときに実行される動作を追加するには、その状態のコールバック メソッドをオーバーライドします。
  • Android Studio でクラスにスケルトン オーバーライド メソッドを追加するには、[Code] > [Override Methods...] を選択するか、Control+O を押します。

Log を使用したロギング

  • Android Logging API(具体的には Log クラス)を使用すると、Android Studio の Logcat に表示される短いメッセージを記述できます。
  • Log.d() を使用してデバッグ メッセージを記述します。このメソッドは、ログタグ(通常はクラスの名前)とログメッセージ(短い文字列)の 2 つの引数を取ります。
  • Android Studio の [Logcat] ウィンドウで、システムログ(自分で記述したメッセージを含む)を表示します。

構成の変更

  • 構成の変更は、デバイスの状態が大きく変更されたために、システムにとってアクティビティを破棄して再構築するのが最も手間がかからない場合に行われます。
  • 構成の変更が発生する最も一般的な例は、ユーザーがデバイスを縦向きから横向きに、または横向きから縦向きにした場合です。デバイスの言語が変更された場合や、ユーザーがハードウェア キーボードを接続した場合も、構成の変更が発生する可能性があります。
  • 構成の変更が発生すると、Android はすべてのアクティビティ ライフサイクルのシャットダウン コールバックを呼び出します。その後、Android はアクティビティをゼロから再起動して、ライフサイクルの起動コールバックをすべて実行します。
  • Android は、構成の変更によりアプリをシャットダウンする際、onCreate() でアクティビティを再開します。
  • 構成の変更後も残す必要がある値を保存するには、rememberSaveable を使用して変数を宣言します。

詳細