如何使用活動生命週期和狀態

1. 歡迎

本實作程式碼研究室屬於第 1 單元:從 Android 開發人員基礎知識 (第 2 版) 課程學習。如依序完成本程式碼研究室的課程,就能充分發揮本課程的價值:

  • 如需課程程式碼研究室的完整清單,請參閱 Android 開發人員基礎知識 (第 2 版) 程式碼研究室。
  • 如需本課程的詳細資訊,包括所有概念章節、應用程式和投影片的連結,請參閱 Android 開發人員基礎知識 (第 2 版)。

簡介

在本練習中,您將進一步瞭解活動生命週期。生命週期是指活動在整個生命週期中經歷的一組狀態,也就是從系統建立活動、刪除活動,到回收活動資源為止。當使用者瀏覽不同的應用程式活動,以及來回切換應用程式時,活動會在生命週期的不同狀態之間轉換。

重複問題

活動生命週期中的每個階段都有相對應的回呼方法,例如 onCreate()、onStart()、onPause() 等。當活動變更狀態時,系統會叫用相關聯的回呼方法。您已經看過以下其中一種方法:onCreate()。覆寫 Activity 類別中的任何生命週期回呼方法,即可變更活動的預設行為,回應使用者或系統動作。

活動狀態也會隨裝置設定變更而變化,例如使用者將裝置從直向轉為橫向時。發生這些設定變更時,系統會刪除活動,並以活動的預設狀態重新建立活動,而使用者可能會失去在活動中輸入的資訊。為避免造成使用者混淆,請務必開發相關應用程式功能,防止資料意外遺失。稍後,您將在本練習中利用設定變更進行實驗,瞭解如何因應裝置設定變更和其他活動生命週期事件,保留活動狀態。

在本練習中,您會在 TwoActivities 應用程式中新增記錄陳述式,觀察活動生命週期的變化。接著您會開始利用這些變更,瞭解如何在這些情況下處理使用者輸入內容。

必要條件

您必須具備以下能力:

  • Android Studio 中建立及執行應用程式專案。
  • 將記錄陳述式新增至應用程式,並在 Logcat 窗格中查看這些記錄。
  • 瞭解並運用活動和意圖,且熟悉如何與這些項目互動。

課程內容

  • 活動生命週期的運作方式。
  • 當 Activity 開始、暫停、停止並刪除時。
  • 與活動變更相關聯的生命週期回呼方法。
  • 設定變更等動作會如何影響活動生命週期事件。
  • 如何跨生命週期事件保留活動狀態。

學習內容

  • 新增程式碼至先前練習中的 TwoActivities 應用程式,實作各種活動生命週期回呼,並納入記錄陳述式。
  • 在應用程式執行期間,以及您與應用程式中每個活動互動時,觀察狀態變更情形。
  • 修改應用程式,為意外重建的 Activity 保留例項狀態,以回應使用者行為或裝置設定變更。

2. 應用程式總覽

在本練習中,您會新增程式碼至 TwoActivities 應用程式。此應用程式的外觀和行為與上一個程式碼研究室中大致相同。這包含兩個 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. 使用 onStart() 方法做為範本,實作 onPause()、onRestart()、onResume()、onStop() 和 onDestroy() 生命週期回呼

所有回呼方法的簽章皆相同 (名稱除外)。如果您複製和貼上 onStart() 來建立這些其他回呼方法,別忘了更新內容以呼叫父類別中的正確的方法,並記錄正確的方法。

  1. 執行應用程式。
  2. 按一下 Android Studio 底部的「Logcat」分頁標籤,就會顯示 Logcat 窗格。您應該會看到三個記錄訊息,顯示活動啟動後所經歷的三個生命週期狀態:
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. 將生命週期回呼和記錄陳述式新增至第二個 Activity。(您可以從 MainActivity 複製及貼上回呼方法)。
  2. 將記錄陳述式新增至 returnReply() 方法之前,位於 complete() 方法之前:
Log.d(LOG_TAG, "End SecondActivity");

1.4 在應用程式執行時觀察記錄內容**

  1. 執行應用程式。
  2. 按一下 Android Studio 底部的「Logcat」分頁標籤,就會顯示 Logcat 窗格。
  3. 在搜尋框中輸入「活動」。Android Logcat 內容可能很冗長且雜亂。由於每個類別的 LOG_TAG 變數都包含 MainActivity 或 SecondActivity 字詞,因此這個關鍵字可用來篩選記錄,只顯示您想瞭解的項目。

重複問題

請使用應用程式進行實驗,並留意因不同動作而產生的生命週期事件。請特別嘗試下列動作:

  • 照常使用應用程式,例如傳送訊息、回覆其他訊息。
  • 使用返回按鈕,從第二個活動返回主要活動。
  • 使用應用程式列的向上箭頭,從第二個活動返回主要活動。
  • 在主要和第二個「活動」中的不同時間旋轉裝置,觀察 * 記錄和畫面上的變化。
  • 按下總覽按鈕 (主畫面右側的正方形按鈕),然後關閉應用程式 (輕觸 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. 4. 工作 2:儲存及還原 Activity 例項狀態

視系統資源和使用者行為而定,應用程式中每個活動遭到刪除及重建的頻率可能會遠超出您的預期。

在上一節中旋轉裝置或模擬器時,您可能已注意到這項行為。旋轉裝置就是裝置設定變更的其中一個例子。雖然旋轉是最常見的設定變更,但所有設定變更都會導致目前的 Activity 遭到刪除,並重新建立為全新活動。如果您的程式碼沒有考量這項行為,那麼在設定變更時,活動版面配置可能會還原為預設外觀和初始值,而使用者可能會失去位置、資料或他們在應用程式中的進度狀態。

每個活動的狀態會以一組鍵/值組合的形式儲存在套件物件中,稱為 Activity 例項狀態。系統會在 Activity 停止前,將預設狀態資訊儲存到例項狀態套件,並將該 Bundle 傳遞至新的 Activity 例項來還原。

為避免在 Activity 意外刪除及重建時遺失資料,您需要實作 onSaveInstanceState() 方法。當系統可能刪除並重新建立 Activity 時,系統會在您的活動中呼叫這個方法 (在 onPause() 和 onStop() 之間)。

您在例項狀態中儲存的資料,只會套用至目前應用程式工作階段內此特定活動的執行個體。停止並重新啟動新的應用程式工作階段時,Activity 例項狀態會遺失,活動也會還原為預設外觀。如果您需要在應用程式工作階段之間儲存使用者資料,請使用共用偏好設定或資料庫。後續練習會進一步說明這兩個概念。

2.1 使用 onSaveInstanceState() 儲存 Activity 例項狀態

您可能已註意到,旋轉裝置完全不會影響第二個活動的狀態。這是因為第二個活動的版面配置和狀態,是由版面配置和啟動該活動的意圖所產生。即使重新建立活動,意圖仍會存在,且每次呼叫第二個 Activity 中的 onCreate() 方法時,該意圖中的資料仍會使用。

此外,您可能會注意到,在每個「活動」中,您輸入至訊息或回覆 EditText 元素的任何文字都會保留,即使裝置旋轉也一樣。這是因為版面配置中部分檢視畫面元素的狀態資訊會在設定變更時自動儲存,而 EditText 目前的值就屬於這類資訊。

因此,您想瞭解的活動狀態只有回覆標頭的 TextView 元素,以及主要 Activity 中的回覆文字。根據預設,這兩個 TextView 元素並不會顯示,只有在您從第二個 Activity 將訊息傳回至主要 Activity 時才會顯示。

在這項工作中,您要新增程式碼,使用 onSaveInstanceState() 保留這兩個 TextView 元素的例項狀態。

  1. 開啟 MainActivity。
  2. 將這個 onSaveInstanceState() 的基本架構實作新增至 Activity,或依序點選「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);
    }

請注意,在出現來自第二個 Activity 的回覆之前,回覆標頭和文字才會標示為隱藏。如果標頭會顯示,就需要儲存回覆資料。請注意,我們只想瞭解顯示狀態,而標頭文字不會變更,所以不需要儲存實際的文字。

  1. 在同一檢查中,將回覆文字新增至「郵件分類」。
outState.putString("reply_text",mReplyTextView.getText().toString());

如果標頭會顯示,您可以假設回覆訊息本身會一併顯示。您不需要測試或儲存回覆訊息目前的顯示狀態。只有訊息的實際文字會進入狀態套件中,並包含「reply_text」鍵。

請只為可能會在 Activity 建立後變更的 View 元素儲存狀態。應用程式中的其他 View 元素 (EditText、按鈕) 隨時可以透過預設版面配置重新建立。

請注意,系統會儲存部分 View 元素的狀態,例如 EditText 的內容。

2.2 在 onCreate() 中還原 Activity 例項狀態

您儲存 Activity 例項狀態後,也需要在重新建立 Activity 時還原狀態。方法是在 onCreate() 中完成,或者實作 onRestoreInstanceState() 回呼 (在 Activity 建立之後於 onStart() 之後呼叫)。

大多數情況下,還原活動狀態的最佳位置是在 onCreate() 中,可確保盡快取得 UI 和狀態。您可以在完成所有初始化作業後於 onRestoreInstanceState() 中執行還原作業,或是讓子類別決定是否要使用預設實作項目,這麼做有時很方便。

  1. 在 onCreate() 方法中,使用 findViewById() 初始化 View 變數後,請新增測試,確認 savedInstanceState 並非空值。
// 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 後,系統會將狀態套件傳送至 onCreate(),做為唯一的引數。系統第一次呼叫 onCreate() 並啟動您的應用程式時,Bundle 是空值;應用程式第一次啟動時並沒有狀態。後續對 onCreate() 的呼叫會包含組合,填入您儲存在 onSaveInstanceState() 中的資料。

  1. 在該檢查中,使用「reply_visible」鍵從套件取得目前的顯示狀態 (true 或 false)。
if (savedInstanceState != null) {
    boolean isVisible = 
                     savedInstanceState.getBoolean("reply_visible");
}
  1. 在前一行的下方,為 isVisible 變數新增測試。
if (isVisible) {
}

如果狀態套件中有 response_visible 鍵 (因此 isVisible 為 true),就需要還原狀態。

  1. 在 isVisible 測試中,讓系統顯示標頭。
mReplyHeadTextView.setVisibility(View.VISIBLE);
  1. 從 Bundle 取得包含「reply_text」鍵的文字回覆訊息,然後將回覆 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. 程式設計

挑戰:建立一個簡單的購物清單應用程式,其中含有使用者正在建立的清單的活動,第二個活動則顯示常見的購物商品清單。

  • 主要活動應包含要建立的清單,而該清單應含有十個空白 TextView 元素。
  • 點選主要活動上的「Add Item」按鈕,即可啟動第二個活動。該活動包含常見的購物商品清單,例如起司、飯食、蘋果等。使用按鈕元素顯示商品。
  • 選擇商品後,系統會將使用者帶回主要活動,並將空白的 TextView 更新為包含所選商品。

使用意圖在一項活動之間傳遞資訊。請確認系統會在使用者旋轉裝置時,儲存購物清單的目前狀態。

6. 摘要

  • 活動生命週期是指活動經歷的一組狀態,從活動首次建立時開始,到 Android 系統回收該活動資源時結束。
  • 當使用者瀏覽不同的活動,以及來回切換應用程式時,每個活動會在活動生命週期中的狀態間切換。
  • 活動生命週期中的每個狀態都有對應的回呼方法,可在 Activity 類別中覆寫。
  • 生命週期方法包括 onCreate()、onStart()、onPause()、onRestart()、onResume()、onStop()、onDestroy()。
  • 覆寫生命週期回呼方法,即可新增會在活動轉換為該狀態時發生的行為。
  • 您可以在 Android Studio 中依序點選「Code」>「Override」,將架構覆寫方法新增至類別。
  • 旋轉等裝置設定變更會導致活動遭到刪除,並重新建立為全新活動。
  • 設定變更時,系統會保留部分「活動」狀態,包括 EditText 元素目前的值。至於所有其他資料,您必須自行明確儲存。
  • 將 Activity 例項狀態儲存在 onSaveInstanceState() 方法中。
  • 例項狀態資料會以簡單的鍵/值組合形式儲存在 Bundle 中。使用 Bundle 方法可將資料放入套件,也可以清空其中的資料。
  • 還原 onCreate() 中的例項狀態,這是建議做法,或是 onRestoreInstanceState()。返回