Android の基礎 02.2: アクティビティのライフサイクルと状態

1. ようこそ

はじめに

この演習では、アクティビティのライフサイクルについて詳しく説明します。ライフサイクルとは、アクティビティが作成されて破棄され、システムがリソースを再利用するまで(すなわちアクティビティの存続期間中)に取り得る一連の状態のことです。ユーザーがアプリのアクティビティ間やアプリの内外を移動すると、アクティビティは、そのライフサイクルのさまざまな状態に遷移します。

アプリのライフサイクルの図

アクティビティのライフサイクルの各ステージには、対応するコールバック メソッド(onCreate()、onStart()、onPause() など)があります。アクティビティの状態が変更されると、関連するコールバック メソッドが呼び出されます。このうち、メソッド onCreate() についてはすでに説明しました。Activity クラスのライフサイクル コールバック メソッドのいずれかをオーバーライドすることで、ユーザーまたはシステムのアクションに応じてアクティビティのデフォルトの動作を変更できます。

アクティビティの状態も、ユーザーがデバイスを縦向きから横向きに回転させたときなど、デバイス設定変更に応じて変化することがあります。このような設定変更が発生すると、アクティビティは破棄され、デフォルトの状態で再作成されます。そのため、ユーザーがアクティビティに入力した情報が失われる可能性があります。ユーザーの混乱を避けるため、予期せぬデータ損失を防ぐためのアプリ開発は重要です。この演習では、後ほど設定変更を試し、デバイス設定変更やその他のアクティビティのライフサイクル イベントに応じてアクティビティの状態を維持する方法について学びます。

この演習では、TwoActivities アプリにロギング ステートメントを追加し、アプリの使用に伴うアクティビティのライフサイクルの変化を確認します。その後、これらの変更と連動して、このような状況下でユーザー入力を処理する方法を探ります。

前提となる知識

次の知識は必須です。

  • Android Studio でアプリ プロジェクトを作成、実行する。
  • アプリにログ ステートメントを追加し、[Logcat] ペインでログを表示する。
  • Activity と Intent について理解したうえで使用し、問題なく操作する。

学習内容

  • Activity のライフサイクルの仕組み。
  • Activity が開始、一時停止、停止、および破棄されるタイミング。
  • Activity の変更に関連するライフサイクル コールバック メソッドについて。
  • Activity のライフサイクル イベントにつながる可能性のあるアクション(設定変更など)の効果。
  • ライフサイクル イベント全体で Activity の状態を保持する方法。

演習内容

  • 以前に演習した TwoActivities アプリにコードを追加して、ロギング ステートメントを含めるためのさまざまな Activity ライフサイクル コールバックを実装する。
  • アプリの実行中およびアプリ内での各 Activity の操作中に、状態がどのように変化するかを確認する。
  • ユーザーの動作やデバイス設定変更に応じて予期せず再作成された Activity インスタンスの状態を保持できるよう、アプリを変更する。

2. アプリの概要

この演習では、TwoActivities アプリに追加します。アプリの外観と動作は、前回の Codelab とほぼ同じです。これには 2 つの Activity の実装が含まれ、ユーザーはそれらの間で送信できるようになります。今回の演習でアプリに変更を加えても、表示されるユーザーの動作には影響しません。

3. タスク 1: TwoActivities にライフサイクル コールバックを追加する

このタスクでは、Activity ライフサイクル コールバック メソッドをすべて実装し、それらのメソッドが呼び出されたときにメッセージを logcat に出力します。これらのログメッセージを見ると、Activity のライフサイクルの状態がいつ変化したか、その変化が実行時のアプリにどのような影響を与えるかが確認できます。

1.1(任意)TwoActivities プロジェクトをコピーする

この演習のタスクでは、前回の演習で作成した既存の TwoActivities プロジェクトを変更します。以前の TwoActivities プロジェクトをそのまま保持する場合は、付録: ユーティリティの手順に沿ってプロジェクトのコピーを作成します。

1.2 MainActivity にコールバックを実装する

  1. Android Studio で TwoActivities プロジェクトを開き、MainActivity ペインで MainActivity を開きます。
  2. onCreate() メソッドに、次のログ ステートメントを追加します。
Log.d(LOG_TAG, "-------");
Log.d(LOG_TAG, "onCreate");
  1. ステートメントを使用して、イベントのログに onStart() コールバックのオーバーライドを追加します。
@Override
public void onStart(){
    super.onStart();
    Log.d(LOG_TAG, "onStart");
}

ショートカットを作成するには、Android Studio で [Code] > [Override Methods] を選択します。ダイアログが開き、クラスでオーバーライドできるすべてのメソッドが表示されます。リストから 1 つ以上のコールバック メソッドを選択すると、それらのメソッド用の完全なテンプレート(スーパークラスへの必要な呼び出しを含む)が挿入されます。

  1. onStart() メソッドをテンプレートとして使用し、onPause()、onRestart()、onResume()、onStop()、onDestroy() のライフサイクル コールバックを実装します。

すべてのコールバック メソッドのシグネチャは同じです(名前を除く)。onStart() をコピー、貼り付けして他のコールバック メソッドを作成するには、必ずスーパークラスで適切なメソッドを呼び出すように内容を更新し、正しいメソッドを記録してください。

  1. アプリを実行します。
  2. Android Studio の下部にある [Logcat] タブをクリックして、[Logcat] ペインを表示します。開始時に Activity が遷移した 3 つのライフサイクル状態を示す 3 つのログメッセージが表示されるはずです。
D/MainActivity: -------
D/MainActivity: onCreate
D/MainActivity: onStart
D/MainActivity: onResume

1.3 SecondActivity にライフサイクル コールバックを実装する

MainActivity のライフサイクル コールバック メソッドを実装したところで、SecondActivity についても同様に行います。

  1. SecondActivity を開きます。
  2. クラスの先頭に、LOG_TAG 変数の定数を追加します。
private static final String LOG_TAG = SecondActivity.class.getSimpleName();
  1. ライフサイクル コールバックとログ ステートメントを 2 つ目の Activity に追加します。(MainActivity のコールバック メソッドをコピーして貼り付けます)。
  2. finish() メソッドの直前にある returnReply() メソッドに、ログ ステートメントを追加します。
Log.d(LOG_TAG, "End SecondActivity");

1.4 アプリの実行中にログを確認する

  1. アプリを実行します。
  2. Android Studio の下部にある [Logcat] タブをクリックして、[Logcat] ペインを表示します。
  3. 検索ボックスに「Activity」と入力します。Android logcat は非常に長く、雑然としたものになる場合があります。各クラスの LOG_TAG 変数には、MainActivity または SecondActivity という単語が含まれているため、このキーワードを使用すると、目的の項目だけが表示されるようにログをフィルタできます。

ライフサイクルの状態を示すログ

アプリを使用してテストし、さまざまなアクションに応じて発生するライフサイクル イベントに注意してください。特に、以下のことを試してみてください。

  • アプリを通常どおりに使用します(メッセージを送信し、別のメッセージで返信します)。
  • 戻るボタンを使用して、2 つ目の Activity からメインの Activity に戻ります。
  • アプリバーの上矢印を使用して、2 つ目の Activity からメインの Activity に戻ります。
  • アプリのメインと 2 つ目の Activity の両方で、デバイスを異なるタイミングで回転させ、ログと画面の内容を確認します。
  • 概要ボタン(ホームの右側にある四角いボタン)を押して、アプリを閉じます([X] をタップします)。
  • ホーム画面に戻り、アプリを再起動します。

ヒント: エミュレータでアプリを実行している場合は、Control+F11 または Control+Function+F11 で回転をシミュレートできます。

タスク 1 の解答コード

次のコード スニペットは、最初のタスクの解答コードを示しています。

MainActivity

次のコード スニペットは、MainActivity に追加したコードを示していますが、クラス全体ではありません。

onCreate() メソッド:

@Override
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Log the start of the onCreate() method.
        Log.d(LOG_TAG, "-------");
        Log.d(LOG_TAG, "onCreate");

        // Initialize all the view variables.
        mMessageEditText = findViewById(R.id.editText_main);
        mReplyHeadTextView = findViewById(R.id.text_header_reply);
        mReplyTextView = findViewById(R.id.text_message_reply);
}

その他のライフサイクル メソッド:

@Override
protected void onStart() {
        super.onStart();
        Log.d(LOG_TAG, "onStart");
}

@Override
protected void onPause() {
        super.onPause();
        Log.d(LOG_TAG, "onPause");
}

@Override
protected void onRestart() {
        super.onRestart();
        Log.d(LOG_TAG, "onRestart");
}

@Override
protected void onResume() {
        super.onResume();
        Log.d(LOG_TAG, "onResume");
}

@Override
protected void onStop() {
        super.onStop();
        Log.d(LOG_TAG, "onStop");
}

@Override
protected void onDestroy() {
        super.onDestroy();
        Log.d(LOG_TAG, "onDestroy");
}

SecondActivity

次のコード スニペットは、SecondActivity に追加したコードを示していますが、クラス全体ではありません。

SecondActivity クラスの先頭で、変数に次の定数を追加します。

private static final String LOG_TAG = SecondActivity.class.getSimpleName();

returnReply() メソッド:

public void returnReply(View view) {
        String reply = mReply.getText().toString();
        Intent replyIntent = new Intent();
        replyIntent.putExtra(EXTRA_REPLY, reply);
        setResult(RESULT_OK, replyIntent);
        Log.d(LOG_TAG, "End SecondActivity");
        finish();
}

その他のライフサイクル メソッド:

上記の MainActivity と同じです。

4. タスク 2: Activity インスタンスの状態を保存して復元する

システム リソースやユーザーの動作によっては、想定よりもはるかに頻繁に、アプリの各 Activity が破棄および再構築される場合があります。

最後のセクションでデバイスまたはエミュレータを回転したとき、この動作に気づいたかもしれません。デバイスを回転することは、デバイス設定変更の一例です。回転は最も一般的な例ですが、あらゆる設定変更によって現在の Activity は破棄され、新規同様に再作成されます。コード内でこの動作を考慮していない場合、設定変更の発生時に Activity レイアウトがデフォルトの外観と初期値に戻され、ユーザーがアプリの場所、データ、または進行中の状態情報を失う可能性があります。

各 Activity の状態は、Activity インスタンスの状態と呼ばれる Bundle オブジェクト内に Key-Value ペアのセットとして保存されます。デフォルトの状態情報は、Activity が停止する直前に、システムによりインスタンスの状態 Bundle に保存され、その Bundle が新しい Activity インスタンスに渡されて復元されます。

予期せず破棄されて再作成される場合、Activity 内のデータが失われないためには、onSaveInstanceState() メソッドを実装する必要があります。Activity が破棄されて再作成される可能性がある場合、システムにより Activity(onPause()~onStop())にこのメソッドが呼び出されます。

インスタンス状態で保存するデータは、現在のアプリ セッションの間、この特定の Activity のインスタンスだけに固有のものです。新しいアプリ セッションを停止して再起動すると、Activity インスタンスの状態は失われ、Activity はデフォルトの外観に戻ります。アプリ セッション間でユーザーデータを保存する必要がある場合は、共有設定またはデータベースを使用します。どちらについても後に学習します。

2.1 onSaveInstanceState() を使用してアクティビティ インスタンスの状態を保存する

デバイスを回転させても、2 番目の Activity の状態にはまったく影響しないことに気付いたかもしれません。これは、2 番目の Activity レイアウトと状態が、レイアウトとそれをアクティブ化した Intent から生成されるためです。Activity が再作成された場合でも Intent は保持され、2 番目の Activity で onCreate() メソッドが呼び出されるたびに、その Intent 内のデータが引き続き使用されます。

さらに各 Activity では、デバイスが回転した場合でも、メッセージや返信の EditText 要素に入力したテキストは保持されます。これは、レイアウト内の一部の View 要素については、その状態情報が構成変更後も自動的に保存されるためで、EditText の現在の値もそうしたケースの一つです。

したがって、注目すべき Activity の状態は、返信ヘッダーの TextView 要素とメイン Activity の返信テキストのみです。デフォルトでは、TextView 要素はいずれも表示されません。これらは、2 番目の Activity からメインの Activity にメッセージを返信した後にのみ表示されます。

このタスクでは、onSaveInstanceState() を使用して、これら 2 つの TextView 要素のインスタンス状態を保持するコードを追加します。

  1. MainActivity を開きます。
  2. onSaveInstanceState() のスケルトン実装を Activity に追加するか、[Code] > [Override Methods] を使用してスケルトン オーバーライドを挿入します。
@Override
public void onSaveInstanceState(Bundle outState) {
          super.onSaveInstanceState(outState);
}
  1. ヘッダーが現在表示されているかどうかを確認し、表示されている場合は、putBoolean() メソッドと "reply_visible" 鍵を使用してその表示状態を Bundle 状態にします。
    if (mReplyHeadTextView.getVisibility() == View.VISIBLE) {
        outState.putBoolean("reply_visible", true);
    }

返信のヘッダーとテキストは、2 番目の Activity からの返信があるまで非表示になります。ヘッダーが表示されている場合は、保存する必要のある応答データがあります。この公開状態だけに注目してください。ヘッダーの実際のテキストは変更されないため、保存する必要はありません。

  1. 同じチェック内で、返信テキストを Bundle に追加します。
outState.putString("reply_text",mReplyTextView.getText().toString());

ヘッダーが表示されている場合、返信メッセージ自体も表示されていると考えられます。返信メッセージの現在の表示状態をテストや保存する必要はありません。メッセージの実際のテキストのみが、"reply_text" 鍵を使用して Bundle 状態に移行します。

Activity の作成後に変更される可能性がある View 要素の状態だけを保存します。アプリの他の View 要素(EditText、Button)は、デフォルトのレイアウトからいつでも再作成できます。

EditText の内容など、一部の View 要素の状態はシステムによって保存されます。

2.2 onCreate() で Activity インスタンスの状態を復元する

Activity インスタンスの状態を保存した後、Activity が再作成されたときにも復元する必要があります。これは onCreate() で行うか、Activity の作成後に onStart() の後で呼び出される onRestoreInstanceState() コールバックを実装して行うことができます。

ほとんどの場合、Activity の状態を復元するのに適した場所は onCreate() です。これにより、その状態を含む UI が可能な限り早く利用できるようになります。すべての初期化が完了後に onRestoreInstanceState() でそれを実行する、またはデフォルト実装の使用の有無をサブクラスが決定できるようにすると、便利な場合があります。

  1. onCreate() メソッドで、View 変数が findViewById() で初期化された後、savedInstanceState が null でないことを確認するテストを追加します。
// Initialize all the view variables.
mMessageEditText = findViewById(R.id.editText_main);
mReplyHeadTextView = findViewById(R.id.text_header_reply);
mReplyTextView = findViewById(R.id.text_message_reply);

// Restore the state.
if (savedInstanceState != null) {
}

Activity が作成されると、システムは Bundle 状態を唯一の引数として onCreate() に渡します。初めて onCreate() が呼び出されてアプリが起動したとき、Bundle は null です。このアプリの初回起動時には既存の状態はありません。後続の onCreate() への呼び出しでは、onSaveInstanceState() に保存したデータが Bundle に入力されます。

  1. そのチェックの内部で "reply_visible" 鍵を使用して、Bundle から現在の公開設定(true または false)を取得します。
if (savedInstanceState != null) {
    boolean isVisible = 
                     savedInstanceState.getBoolean("reply_visible");
}
  1. 前の行の下に isVisible 変数のテストを追加します。
if (isVisible) {
}

Bundle 状態に reply_visible 鍵がある(そのため isVisible が true である)場合、状態を復元する必要があります。

  1. isVisible テスト内でヘッダーを表示します。
mReplyHeadTextView.setVisibility(View.VISIBLE);
  1. "reply_text" 鍵を使用して Bundle からテキスト返信メッセージを受け取り、その返信を TextView に設定して文字列を表示します。
mReplyTextView.setText(savedInstanceState.getString("reply_text"));
  1. 返信の TextView も表示されるようにします。
mReplyTextView.setVisibility(View.VISIBLE);
  1. アプリを実行します。デバイスまたはエミュレータを回転させて、Activity が再作成された後に返信メッセージ(存在する場合)が画面に残るようにします。

タスク 2 の解答コード

次のコード スニペットは、このタスクの解答コードを示しています。

MainActivity

次のコード スニペットは、MainActivity に追加したコードを示していますが、クラス全体ではありません。

onSaveInstanceState() メソッド:

@Override
public void onSaveInstanceState(Bundle outState) {
   super.onSaveInstanceState(outState);
   // If the heading is visible, message needs to be saved.
   // Otherwise we're still using default layout.
   if (mReplyHeadTextView.getVisibility() == View.VISIBLE) {
       outState.putBoolean("reply_visible", true);
       outState.putString("reply_text", 
                      mReplyTextView.getText().toString());
   }
}

onCreate() メソッド:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);

   Log.d(LOG_TAG, "-------");
   Log.d(LOG_TAG, "onCreate");

   // Initialize all the view variables.
   mMessageEditText = findViewById(R.id.editText_main);
   mReplyHeadTextView = findViewById(R.id.text_header_reply);
   mReplyTextView = findViewById(R.id.text_message_reply);

   // Restore the saved state. 
   // See onSaveInstanceState() for what gets saved.
   if (savedInstanceState != null) {
       boolean isVisible = 
                     savedInstanceState.getBoolean("reply_visible");
       // Show both the header and the message views. If isVisible is
       // false or missing from the bundle, use the default layout.
       if (isVisible) {
           mReplyHeadTextView.setVisibility(View.VISIBLE);
           mReplyTextView.setText(savedInstanceState
                                  .getString("reply_text"));
           mReplyTextView.setVisibility(View.VISIBLE);
       }
   }
}

プロジェクト全体:

Android Studio プロジェクト: TwoActivitiesLifecycle

5. コーディングの課題

課題: ユーザーが作成するリスト用のメイン アクティビティと、一般的なショッピング アイテム リスト用の 2 つ目のアクティビティを備えたシンプルなショッピング リスト アプリを作成する。

  • メイン アクティビティには作成するリストが含まれ、このリストは 10 個の空の TextView 要素で構成される必要があります。
  • メイン アクティビティのアイテムを追加ボタンをクリックすると、一般的なショッピング アイテム(チーズ、米、りんごなど)のリストを含む 2 つ目のアクティビティが開始されます。Button 要素を使用してアイテムを表示します。
  • アイテムを選択すると、ユーザーはメイン アクティビティに戻り、空の TextView が更新されて選択したアイテムが含まれます。

Intent を使用して、Activity 間で情報を受け渡しします。ユーザーがデバイスを回転させたときに、ショッピング リストの現在の状態が保存されているようにします。

6. まとめ

  • アクティビティのライフサイクルは Activity が最初に作成されたときに始まり、それが移行して、その Activity リソースが Android システムによって再利用されたときに終了するまでの一連の状態です。
  • ユーザーが、Activity 間やアプリの内外を移動すると、各 Activity が Activity ライフサイクルの状態間を移行します。
  • Activity ライフサイクルの各状態には、Activity クラスでオーバーライド可能な、対応するコールバック メソッドがあります。
  • ライフサイクル メソッドは、onCreate()、onStart()、onPause()、onRestart()、onResume()、onStop()、onDestroy() です。
  • ライフサイクル コールバック メソッドをオーバーライドすると、Activity がその状態に遷移したときに発生する動作を追加できます。
  • [Code] > [Override] を使用すると、Android Studio でクラスにスケルトン オーバーライド メソッドを追加できます。
  • 回転などのデバイス構成の変更を加えると、新規の場合と同様に Activity が破棄され再作成されます。
  • 構成変更時、Activity の状態の一部(EditText 要素の現在の値など)は保持されます。他のすべてのデータについては、自分で明示的に保存する必要があります。
  • Activity インスタンスの状態を onSaveInstanceState() メソッドに保存します。
  • インスタンスの状態データは、シンプルな Key-Value ペアとして Bundle に保存されます。Bundle メソッドを使用してデータを入れ、Bundle からデータを取得します。
  • onCreate()(推奨)か、または onRestoreInstanceState() でインスタンスの状態を復元します。

7. 関連概念

関連概念のドキュメントについては、「2.2: アクティビティのライフサイクルと状態」をご覧ください。

8. 関連リンク

Android Studio のドキュメント:

Android デベロッパー ドキュメント:

9. 宿題

このセクションでは、インストラクター主導のコースの一環として、この Codelab に取り組んでいる生徒向けに考えられる宿題をいくつか示します。インストラクターは、以下のようなことを行えます。

  • 必要に応じて宿題を与える
  • 宿題の提出方法を生徒に伝える
  • 宿題を採点する

インストラクターは、これらの提案を必要なだけ使用し、必要に応じて他の宿題も自由に与えることができます。

この Codelab に独力で取り組む場合は、これらの宿題を自由に使用して知識をテストしてください。

アプリをビルドして実行する

  1. カウンタ TextView、カウンタをインクリメントする Button、および EditText を保持するレイアウトでアプリを作成します。以下のスクリーンショットは一例です。レイアウトを正確にコピーする必要はありません。
  2. カウンタをインクリメントする Button のクリック ハンドラを追加します。
  3. アプリを実行し、カウンタをインクリメントします。EditText にテキストを入力します。
  4. デバイスを回転させます。カウンタはリセットされますが、EditText はリセットされません。
  5. onSaveInstanceState() を実装して、アプリの現在の状態を保存します。
  6. onCreate() を更新して、アプリの状態を復元します。
  7. デバイスを回転させたときに、アプリの状態が保持されるようにしてください。

ebaf84570af6dd46.png

以下の質問に回答してください

問題 1

onSaveInstanceState() を実装する前に宿題のアプリを実行すると、デバイスを回転させた場合にどうなりますか?いずれかを選択してください。

  • EditText に入力したテキストは削除されるが、カウンタは保持される。
  • カウンタが 0 にリセットされ、EditText に入力したテキストは削除される。
  • カウンタは 0 にリセットされるが、EditText の内容は保持される。
  • カウンタも EditText の内容も保持される。

問題 2

デバイス構成の変更(回転など)が発生したとき、どのような Activity ライフサイクル メソッドが呼び出されますか?いずれかを選択してください。

  • Android は onStop() を呼び出して Activity を直ちにシャットダウンする。コードで Activity を再起動する必要がある。
  • Android は onPause()、onStop()、onDestroy() を呼び出して Activity をシャットダウンする。コードで Activity を再起動する必要がある。
  • Android は onPause()、onStop()、onDestroy() を呼び出して Activity をシャットダウンする。その後、再起動して onCreate()、onStart()、onResume() を呼び出す。
  • Android は直ちに onResume() を呼び出す。

問題 3

Activity のライフサイクルでは、どのタイミングで onSaveInstanceState() が呼び出されますか?いずれかを選択してください。

  • onSaveInstanceState() は、onStop() メソッドの前に呼び出される。
  • onSaveInstanceState() は、onResume() メソッドの前に呼び出される。
  • onSaveInstanceState() は、onCreate() メソッドの前に呼び出される。
  • onSaveInstanceState() は、onDestroy() メソッドの前に呼び出される。

質問 4

Activity が終了または破棄される前に、データを保存するのに最適な Activity ライフサイクル メソッドは次のうちどれですか?いずれかを選択してください。

  • onPause() または onStop()
  • onResume() または onCreate()
  • onDestroy()
  • onStart() または onRestart()

採点のためアプリを送信する

採点者のガイダンス

アプリに以下の機能があることを確認します。

  • カウンタ、そのカウンタをインクリメントする Button、EditText が表示される。
  • Button をクリックすると、カウンタが 1 ずつ増える。
  • デバイスを回転させると、カウンタと EditText の状態がともに保持される。
  • MainActivity.java の実装で、onSaveInstanceState() メソッドを使用してカウンタ値を保存する。
  • onCreate() の実装で、outState Bundle が存在するかどうかをテストする。Bundle が存在する場合、カウンタ値は復元され、TextView に保存される。

10. 次の Codelab

Android デベロッパー向け基礎(V2)コースで、次の実践的な Codelab を確認するには、Android デベロッパー向け基礎(V2)の Codelab をご覧ください。

概念の章、アプリ、スライドへのリンクを含むコースの概要については、Android デベロッパーの基礎(バージョン 2)をご覧ください。