處理設定變更

部分裝置設定可能會在應用程式執行期間變更。這些設定包括但不限於:

  • 應用程式顯示大小
  • 螢幕方向
  • 字型大小和粗細
  • 語言代碼
  • 深色模式與淺色模式
  • 鍵盤可用性

這些設定變更大多是由使用者互動而產生。舉例來說,旋轉或折疊裝置會變更應用程式可用的螢幕空間。同樣地,改變字型大小、語言或偏好的主題等裝置設定,也會變更 Configuration 物件中各自對應的值。

這些參數通常需要對應用程式 UI 進行大幅變更,因此 Android 平台為這些變更建構了專門的機制。這項機制就是「Activity 重建」

重建活動

發生設定變更時,系統會重新建立 Activity。為此,系統會呼叫 onDestroy() 並刪除現有的 Activity 例項,接著使用 onCreate() 建立新的例項,並運用新的更新設定初始化這個新的 Activity 例項。也就是說,系統也會使用新設定重建使用者介面。

重建行為可讓應用程式根據與新裝置設定相符的替代資源自動重新載入應用程式,協助應用程式適應新設定

重建範例

假設 TextView 會使用 android:text="@string/title" 顯示靜態標題,如版面配置 XML 檔案所定義。檢視區塊會在建立時根據目前語言,僅設定一次文字。如果語言有變,系統會重新建立活動。因此,系統也會重建檢視畫面,並根據新語言將檢視畫面初始化為正確的值。

重建作業也會清除在 Activity 或其任何內含 FragmentView 或其他物件中,以欄位形式保留的任何狀態。這是因為 Activity 重建會建立全新的 Activity 例項和 UI。此外,舊的 Activity 已經不會再顯示或是已經失效,因此任何對該活動或其內含物件的參照均已過時。這些參照可能會導致發生錯誤、記憶體流失和當機情形。

使用者的期望

應用程式的使用者會預期狀態能夠保留。如果使用者正在填寫表單,並且以多視窗模式開啟另一個應用程式來參照資訊,則當使用者返回到已清除內容的表單,或應用程式的其他位置,就會造成使用者體驗不佳。開發人員必須透過設定變更和活動重建,提供一致的使用者體驗。

如要驗證應用程式是否能保留狀態,可以在應用程式於前景和背景運作時,執行會導致設定變更的動作。這些動作包括:

  • 旋轉裝置
  • 進入多視窗模式
  • 在多視窗模式或任意形式視窗中調整應用程式大小
  • 折疊搭配多個螢幕的折疊式裝置
  • 變更系統主題,例如深色模式與淺色模式
  • 變更字型大小
  • 變更系統或應用程式語言
  • 連接或拔除硬體鍵盤
  • 連接或拔除座架

您可以採用三種主要方式,透過 Activity 重建來保留相關狀態。要使用的方式視您想保留的狀態類型而定:

  • 本機常駐性可用於處理複雜或大型資料的程序終止。常駐性本機儲存空間包含資料庫或 DataStore
  • 保留的物件 (例如 ViewModel 例項),可在使用者積極使用應用程式時,處理記憶體中的 UI 相關狀態。
  • 儲存的例項狀態,用於處理系統啟動的程序終止,並根據使用者輸入內容或瀏覽情形保留暫時狀態。

如要進一步瞭解每種 API 的 API,以及何時適用,請參閱「儲存 UI 狀態」。

限制活動重建

您可以避免系統為特定設定變更自動重建活動。Activity 重建會導致系統重新建立整個 UI,以及衍生自 Activity 的所有物件。您可能有充分的理由來避免這種情況。舉例來說,應用程式可能不需要在特定設定變更期間更新資源,或者設有效能限制。在此情況下,您可以宣告活動會自行處理設定變更,避免系統重新啟動活動。

如要為特定設定變更停用活動重建功能,請將設定類型加入 AndroidManifest.xml 檔案 <activity> 項目中的 android:configChanges。可能的值會列在 android:configChanges 屬性的說明文件中。

以下資訊清單程式碼會在螢幕方向和鍵盤可用性變更時,停用 MyActivityActivity 重建功能:

<activity
    android:name=".MyActivity"
    android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
    android:label="@string/app_name">

有些設定變更一律會導致系統重新啟動活動,您無法予以停用。例如,您無法停用 Android 12L (API 級別 32) 中導入的動態色彩變更

回應 View 系統中的設定變更

View 系統中,當發生設定變更而您已停用 Activity 重建功能時,該活動會收到對 Activity.onConfigurationChanged() 的呼叫。所有附加的檢視畫面也會收到對 View.onConfigurationChanged() 的呼叫。如果是尚未新增到 android:configChanges 的設定變更,系統會照常重新建立活動。

onConfigurationChanged() 回呼方法會接收指定新裝置設定的 Configuration 物件。請讀取 Configuration 物件中的欄位來判斷新的設定。如要進行後續變更,請更新介面所用的資源。當系統呼叫此方法時,系統會根據新設定,更新活動的 Resources 物件以傳回資源。這樣您就能重設 UI 元素,不需要系統重新啟動活動。

舉例來說,下列 onConfigurationChanged() 實作會檢查鍵盤是否可用:

Kotlin

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)

    // Checks whether a keyboard is available
    if (newConfig.keyboardHidden === Configuration.KEYBOARDHIDDEN_YES) {
        Toast.makeText(this, "Keyboard available", Toast.LENGTH_SHORT).show()
    } else if (newConfig.keyboardHidden === Configuration.KEYBOARDHIDDEN_NO) {
        Toast.makeText(this, "No keyboard", Toast.LENGTH_SHORT).show()
    }
}

Java

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

    // Checks whether a keyboard is available
    if (newConfig.keyboardHidden == Configuration.KEYBOARDHIDDEN_YES) {
        Toast.makeText(this, "Keyboard available", Toast.LENGTH_SHORT).show();
    } else if (newConfig.keyboardHidden == Configuration.KEYBOARDHIDDEN_NO){
        Toast.makeText(this, "No keyboard", Toast.LENGTH_SHORT).show();
    }
}

如果不需要根據這些設定變更更新應用程式,您可以改為不實作 onConfigurationChanged()。在此情況下,所有在設定變更前使用的資源都仍會繼續使用,只是不會重新啟動活動而已。舉例來說,在連接或卸離藍牙鍵盤時,電視應用程式可能不需要回應。

保留狀態

使用這項技巧時,您仍須在一般活動生命週期中保留狀態。可能的原因如下:

  • 不可避免的變更:無法避免的設定變更可能會重新啟動應用程式。
  • 程序終止:應用程式必須能夠處理系統啟動的程序終止。如果使用者退出應用程式,且應用程式進入背景,系統可能會刪除該應用程式。

回應 Jetpack Compose 中的設定變更

Jetpack Compose 可讓應用程式更輕鬆地回應設定變更。但是,如果在可行的情況下為所有設定變更停用 Activity 重建功能,應用程式仍必須正確處理設定變更。

使用 LocalConfiguration 本機組合時,Compose UI 階層可適用 Configuration 物件。每次此物件變更時,從 LocalConfiguration.current 讀取的可組合函式都會重組。如要瞭解本機組合的運作方式,請參閱「使用 CompositionLocal 的本機範圍資料」相關說明。

範例

在以下範例中,可組合項會顯示具有特定格式的日期。該可組合函式會使用 LocalConfiguration.current 呼叫 ConfigurationCompat.getLocales(),回應系統語言代碼設定的變更。

@Composable
fun DateText(year: Int, dayOfYear: Int) {
    val dateTimeFormatter = DateTimeFormatter.ofPattern(
        "MMM dd",
        ConfigurationCompat.getLocales(LocalConfiguration.current)[0]
    )
    Text(
        dateTimeFormatter.format(LocalDate.ofYearDay(year, dayOfYear))
    )
}

為避免在語言代碼變更時重建 Activity,代管 Compose 程式碼的 Activity 需要選擇停用語言代碼設定變更。如要這樣做,請將 android:configChanges 設為 locale|layoutDirection

設定變更:重要概念和最佳做法

處理設定變更時,您需要瞭解下列重要概念:

  • 設定:裝置設定會定義 UI 向使用者顯示的方式,例如應用程式顯示大小、語言代碼或系統主題。
  • 設定變更:設定會隨著使用者互動而變更。例如,使用者可能會變更裝置設定或與裝置實際互動的方式。您無法防止設定變更。
  • Activity 重建:在預設情況下,設定變更會導致 Activity 重建。這項內建機制可針對新設定重新初始化應用程式狀態。
  • Activity 刪除: Activity 重建會導致系統刪除舊 Activity 例項並建立新例項。舊例項現已過時,任何剩餘的參照都會導致記憶體流失、發生錯誤或當機。
  • 狀態:Activity 例項中的狀態不會顯示在新 Activity 例項中,因為這是兩個不同的物件例項。請保留應用程式和使用者的狀態,如「儲存 UI 狀態」一文所述。
  • 選擇停用:為特定設定變更類型選擇停用活動重建功能,是一種潛在的最佳化機制。您需要確保應用程式能正確更新,回應新的設定。

為提供良好的使用者體驗,請留意下列最佳做法:

  • 為頻繁的設定變更做好準備:無論 API 級別、板型規格或 UI 工具包為何,請勿假設設定變更很少發生或完全不會發生。當使用者造成設定變更時,會期望應用程式能夠更新並繼續根據新設定正確運作。
  • 保留狀態:Activity 重建發生時,不要遺失使用者的狀態。請保留「儲存 UI 狀態」一文所述的狀態。
  • 避免將選擇停用做為快速修正方式:請勿將選擇停用 Activity 重建功能,當做避免遺失狀態的捷徑。停用活動重建功能必須履行處理變更的承諾,而您可能會因為其他設定變更、程序終止或關閉應用程式的 Activity 重建而遺失狀態。因此,您無法完全停用 Activity 重建功能。請保留「儲存 UI 狀態」一文所述的狀態。
  • 不要避免設定變更:不要為了避免設定變更或 Activity 重建,對螢幕方向、顯示比例或是否可調整大小設下限制。這會對想以自己偏好方式使用應用程式的使用者產生負面影響。

處理以大小為依據的設定變更

以大小為依據的設定變更隨時都會發生,如果應用程式在大螢幕裝置上執行,當使用者進入多視窗模式時就更有可能發生。使用者預期應用程式能夠在該環境中順利運作。

大小變更分為兩種一般類型:大幅變更和小幅變更。「大幅」的大小變更是指由於螢幕大小的差異 (例如寬度、高度或最小寬度),將不同額外資源組合套用到新設定的情形。這些資源包括應用程式本身定義的資源,以及任何資源提供的程式庫。

針對以大小為依據的設定變更限制活動重建

如果您針對以大小為依據的設定變更停用 Activity 重建功能,系統不會重新建立 Activity,而會改為接收對 Activity.onConfigurationChanged() 的呼叫。所有附加的檢視畫面都會收到對 View.onConfigurationChanged() 的呼叫。

如果資訊清單檔案中設有 android:configChanges="screenSize|smallestScreenSize|orientation|screenLayout",系統會針對以大小為依據的設定變更,停用 Activity 重建功能。

允許針對以大小為依據的設定變更重建活動

在 Android 7.0 (API 級別 24) 以上版本中,只有在尺寸出現大幅變更時,以大小為依據的設定變更才會重建 Activity如果系統因為大小不足而未重建 Activity,可能會改為呼叫 Activity.onConfigurationChanged()View.onConfigurationChanged()

未重建 Activity 時,請留意下列 ActivityView 回呼的相關事項:

  • 在 Android 11 (API 級別 30) 至 Android 13 (API 級別 33) 版本中,系統不會呼叫 Activity.onConfigurationChanged()
  • 目前已知的問題是,在某些情況下,Android 12L (API 級別 32) 和 Android 13 (API 級別 33) 的初期版本可能無法呼叫 View.onConfigurationChanged()。詳情請參閱這個公開問題。 這個問題在後期的 Android 13 版本和 Android 14 中已經解決。

如果程式碼需要監聽以大小為依據的設定變更,建議您使用公用程式 View 搭配覆寫的 View.onConfigurationChanged(),而不要依賴 Activity 重建或 Activity.onConfigurationChanged()