Android 基礎知識 02.2:活動生命週期和狀態

1. 歡迎

簡介

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

應用程式生命週期圖表

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

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

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

必備知識

您必須具備以下能力:

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

課程內容

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

執行步驟

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

2. 應用程式總覽

在本練習中,您會新增程式碼至 TwoActivities 應用程式。此應用程式的外觀和行為與上一個程式碼研究室中大致相同。這包含兩個 Activity 實作項目,可讓使用者在這兩個項目之間傳送內容。您在本練習中對應用程式所做的變更,不會影響使用者看到的應用程式行為。

3. 工作 1:將生命週期回呼新增至 TwoActivities

在這項工作中,您要實作所有 Activity 生命週期回呼方法,讓系統在叫用這些方法時於 Logcat 顯示訊息。這些記錄訊息可讓您瞭解 Activity 生命週期變更狀態的時機,以及這類變更在應用程式執行期間造成的影響。

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」窗格。您應該會看到三個記錄訊息,顯示 Activity 啟動後所經歷的三個生命週期狀態:
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. 將記錄陳述式新增至 finish() 方法之前的 returnReply() 方法:
Log.d(LOG_TAG, "End SecondActivity");

1.4 在應用程式執行期間觀察記錄內容

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

顯示生命週期狀態的記錄

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

  • 照常使用應用程式,例如傳送訊息、回覆其他訊息。
  • 使用返回按鈕,從第二個 Activity 返回主要 Activity
  • 使用應用程式列的向上箭頭,從第二個 Activity 返回主要 Activity
  • 在主要和第二個應用程式 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 的狀態都會以一組鍵/值組合的形式儲存在 Bundle 物件中,該物件稱為 Activity「例項狀態」。系統會在 Activity 停止前,將預設狀態資訊儲存到例項狀態 Bundle,並將 Bundle 傳遞至新的 Activity 例項來還原。

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

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

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

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

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

因此,您想瞭解的 Activity 狀態只有回覆標頭的 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" 鍵,將其顯示狀態放入狀態 Bundle
    if (mReplyHeadTextView.getVisibility() == View.VISIBLE) {
        outState.putBoolean("reply_visible", true);
    }

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

  1. 在同一檢查中,將回覆文字新增至 Bundle
outState.putString("reply_text",mReplyTextView.getText().toString());

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

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

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

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

您儲存 Activity 例項狀態後,也需要在 Activity 重新建立時還原狀態。方法是在 onCreate() 中還原,或是實作 onRestoreInstanceState() 回呼。系統會在 Activity 建立後先呼叫 onStart(),接著再呼叫此回呼。

大多數情況下,還原 Activity 狀態的最佳位置是在 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 建立後,系統會將狀態 Bundle 傳遞至 onCreate(),當做唯一的引數。系統第一次呼叫 onCreate() 並啟動應用程式時,Bundlenull。也就是說,應用程式首次啟動時並沒有狀態。後續呼叫 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. 程式設計挑戰

挑戰:請建立簡單的購物清單應用程式,而主要活動應用於使用者正在建立的清單,第二個活動則用於一般購物商品的清單。

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

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

6. 摘要

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

7. 相關概念

相關概念說明文件請參閱「2.2:活動生命週期和狀態」。

8. 瞭解詳情

Android Studio 說明文件:

Android 開發人員說明文件:

9. 作業

本節列出的作業可由課程講師指派給學習本程式碼研究室的學員。講師可自由採取以下行動:

  • 視需要指派作業。
  • 告知學員如何繳交作業。
  • 為作業評分。

講師可以視需求使用全部或部分建議內容,也可以自由指派任何其他合適的作業。

如果您是自行學習本程式碼研究室,不妨利用這些作業驗收學習成果。

建構並執行應用程式

  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. 下一個程式碼研究室

如要找到 Android 開發人員基礎知識 (第 2 版) 課程中的下一個實作程式碼研究室,請參閱「Android 開發人員基礎知識程式碼研究室 (第 2 版)」。

如需課程總覽,包括概念章節、應用程式和投影片的連結,請參閱「Android 開發人員基礎知識 (第 2 版)」。