アクティビティのライフサイクルと状態を使用する方法

1. ようこそ

この実践的な Codelab は、Android デベロッパー向け基礎(バージョン 2)コースのユニット 1: スタートガイドの一部です。順番に Codelab を進めると、このコースを最大限に活用できます。

  • コースの Codelab の一覧については、Android デベロッパー向け基礎(V2)の Codelab をご覧ください。
  • すべてのコンセプトの章、アプリ、スライドへのリンクを含むコースの詳細については、Android デベロッパーの基礎(バージョン 2)をご覧ください。

はじめに

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

IDouble trouble

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

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

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

前提条件

次のことを行える必要があります。

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

学習内容

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

演習内容

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

2. アプリの概要

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

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

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

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

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

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

  1. Android Studio で TwoActivities プロジェクトを開き、[Project] > [Android] ペインで 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] ペインを表示します。開始時にアクティビティが遷移した 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 つ目のアクティビティに追加します。(MainActivity のコールバック メソッドをコピーして貼り付けることができます)。
  2. returnReply() メソッドの finish() メソッドの直前に、ログ ステートメントを追加します。
Log.d(LOG_TAG, "End SecondActivity");

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

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

IDouble trouble

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

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

ヒント: エミュレータでアプリを実行している場合は、Ctrl+F11 または Ctrl+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. 4. タスク 2: Activity インスタンスの状態を保存して復元する

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

アクティビティ インスタンスの状態を保存した後、アクティビティが再作成されたときにも復元する必要があります。これは onCreate() で行うか、アクティビティの作成後に 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) {
}

アクティビティが作成されると、システムは状態 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. アプリを実行します。デバイスまたはエミュレータを回転させて、アクティビティが再作成された後に返信メッセージ(存在する場合)が画面に残るようにします。

タスク 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 が更新されて選択したアイテムが含まれます。

インテントを使用して、あるアクティビティから別のアクティビティに情報を渡します。ユーザーがデバイスを回転させたときに、ショッピング リストの現在の状態が保存されているようにします。

6. まとめ

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