1. 簡介
Android 裝置生態系統不斷與時俱進。從早期內建硬體鍵盤的裝置,到現代的翻蓋式裝置、摺疊式裝置、平板電腦,甚至是可調整大小的任意形式視窗,今日支援 Android 應用程式的裝置種類可說是五花八門。
雖然這對開發人員來說是好消息,但如要滿足使用者對可用性的期望,在各種大小的裝置上提供優異的使用者體驗,就勢必得將應用程式最佳化。不過,你不必一次針對一種新裝置進行調整,而可採用回應式/自動調整式 UI 和彈性架構,讓應用程式在任何大小/形狀的現有/未來裝置上,都能保持美觀並順利運作。
藉由初步探索可調整大小的任意形式 Android 環境,你可以對自己的回應式/自動調整式 UI 進行壓力測試,確保 UI 在任何裝置上都能正常顯示。本程式碼研究室將引導你瞭解調整大小的重要性,以及如何採用最佳做法,確保應用程式可輕鬆妥善地調整大小。
建構目標
你將瞭解任意形式大小調整的重要性,並採用調整大小的最佳做法將 Android 應用程式最佳化。你的應用程式將會:
有相容的資訊清單
- 移除相關限制,讓應用程式能任意調整大小
在調整大小時維持 UI 狀態
- 使用 rememberSaveable,在調整大小時維持 UI 狀態
- 避免在初始化 UI 時進行不必要的重複背景作業
課程需求
- 建立基本 Android 應用程式的相關知識
- Compose 中 ViewModel 和狀態的相關知識
- 支援任意形式視窗大小調整的測試裝置,例如:
- 具有 ADB 設定的 Chromebook
- 支援 Samsung DeX 模式或 Productivity 模式的平板電腦
- Android Studio 中的電腦版 Android 虛擬裝置模擬器
進行本程式碼研究室時,如果你遇到任何問題 (例如程式碼錯誤、文法錯誤或用詞不明確等),請透過程式碼研究室左下角的「回報錯誤」連結回報問題。
2. 踏出第一步
從 GitHub 複製存放區。
git clone https://github.com/android/large-screen-codelabs/
你也可以下載存放區的 ZIP 檔案並解壓縮
匯入專案
- 開啟 Android Studio
- 選擇「Import Project」,或依序選取「File」->「New」->「Import Project」
- 前往你複製或解壓縮專案的位置
- 開啟「resizing」資料夾
- 開啟「start」資料夾中的專案,當中包含範例程式碼
嘗試調整應用程式大小
- 建構並執行應用程式
- 嘗試調整應用程式大小
結果如何?
視測試裝置的相容性而定,你可能會發現使用者體驗不甚理想。應用程式無法調整大小,一直維持初始顯示比例。為什麼會這樣呢?
資訊清單限制
如果你查看應用程式的 AndroidManifest.xml
檔案,可能會發現當中加入了幾項限制,導致應用程式無法在任意形式視窗大小調整環境中正常運作。
AndroidManifest.xml
android:maxAspectRatio="1.4"
android:resizeableActivity="false"
android:screenOrientation="portrait">
請從資訊清單中移除造成問題的這幾行程式碼,重新建構應用程式,然後在測試裝置上再試一次。你會發現應用程式可以進行任意形式大小調整了。從資訊清單中移除這類限制,是讓應用程式能夠進行任意形式視窗大小調整的重要步驟。
3. 大小調整設定變更
應用程式視窗調整大小時,應用程式的設定會隨之更新,進而影響應用程式。建議你瞭解這些影響並做好準備,以便提供優異的使用者體驗。最明顯的變化是應用程式視窗的寬度和高度,但顯示比例和螢幕方向也會受到影響。
觀察設定變化
如要自行在透過 Android View 系統建構的應用程式中觀察相關變化,你可以覆寫 View.onConfigurationChanged
。在 Jetpack Compose 中,我們可以存取 LocalConfiguration.current
,其會在系統呼叫 View.onConfigurationChanged
時自動更新。
如要在範例應用程式中觀察這些設定變化,請在應用程式中新增會顯示 LocalConfiguration.current
值的可組合函式,或是建立包含這類可組合函式的新範例專案。可觀察相關變化的範例 UI 看起來如下所示:
val configuration = LocalConfiguration.current
val isPortrait = configuration.orientation ==
Configuration.ORIENTATION_PORTRAIT
val screenLayoutSize =
when (configuration.screenLayout and
Configuration.SCREENLAYOUT_SIZE_MASK) {
SCREENLAYOUT_SIZE_SMALL -> "SCREENLAYOUT_SIZE_SMALL"
SCREENLAYOUT_SIZE_NORMAL -> "SCREENLAYOUT_SIZE_NORMAL"
SCREENLAYOUT_SIZE_LARGE -> "SCREENLAYOUT_SIZE_LARGE"
SCREENLAYOUT_SIZE_XLARGE -> "SCREENLAYOUT_SIZE_XLARGE"
else -> "undefined value"
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Text("screenWidthDp: ${configuration.screenWidthDp}")
Text("screenHeightDp: ${configuration.screenHeightDp}")
Text("smallestScreenWidthDp: ${configuration.smallestScreenWidthDp}")
Text("orientation: ${if (isPortrait) "portrait" else "landscape"}")
Text("screenLayout SIZE: $screenLayoutSize")
}
「observing-configuration-changes」專案資料夾中包含實作範例。請將該範例加入應用程式的 UI,在測試裝置上執行,並觀察 UI 如何在應用程式設定變化時隨之更新。
這些應用程式設定變化可讓你快速模擬各種情境,包括小型手機上的分割畫面模式等極端情況,或是平板電腦或桌上型電腦上的全螢幕模式。這不僅是在各種裝置上測試應用程式版面配置的好方法,還可讓你測試應用程式因應快速設定變化事件的情況。
4. 記錄活動生命週期事件
應用程式任意形式視窗大小調整的另一個影響,在於應用程式會發生各種 Activity
生命週期變化。如要即時觀察這類變化,請在 onCreate
方法中新增生命週期觀察器,並覆寫 onStateChanged
來記錄每項新的生命週期事件。
lifecycle.addObserver(object : LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
Log.d("resizing-codelab-lifecycle", "$event was called")
}
})
完成記錄設定後,請再次在測試裝置上執行應用程式,然後將應用程式最小化並再次顯示於前景,同時查看 Logcat。
你會看到應用程式在最小化時暫停運作,並在顯示於前景時繼續運作。這會對應用程式造成影響,詳情請參閱本程式碼研究室中後續關於連續性的部分。
現在請查看 Logcat,瞭解當你將應用程式的大小從最小調整到最大時,系統呼叫了哪些活動生命週期回呼。
視測試裝置而定,實際觀察到的行為可能會有所不同,但你大概已注意到當應用程式視窗大小大幅度改變時,系統會刪除並重新建立活動,但如果視窗大小僅小幅度改變,則不會發生這類情形。這是因為在 API 24 以上版本中,只有在大小大幅度改變時,系統才會重新建立 Activity
。
你已瞭解任意形式視窗環境中的幾種常見設定變化,但還有其他要注意的變化。舉例來說,如果你將外接式螢幕連接至測試裝置,系統可能會根據設定變化 (例如顯示密度) 刪除並重新建立 Activity
。
如要簡化與設定變化相關的複雜作業,請使用 WindowSizeClass 等較高層級的 API 實作自動調整式 UI (另請參閱這個網頁)。
5. 連續性 - 在應用程式調整大小時維持可組合函式的內部狀態
在上一個部分中,你已瞭解在任意形式視窗大小調整環境中,應用程式可能會發生的設定變化。在這個部分中,你要讓應用程式的 UI 狀態在經歷這些變化時維持不變。
首先請讓 NavigationDrawerHeader
可組合函式 (位於 ReplyHomeScreen.kt
中) 展開,在點按時顯示電子郵件地址。
@Composable
private fun NavigationDrawerHeader(
modifier: Modifier = Modifier
) {
var showDetails by remember { mutableStateOf(false) }
Column(
modifier = modifier.clickable {
showDetails = !showDetails
}
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
ReplyLogo(
modifier = Modifier
.size(dimensionResource(R.dimen.reply_logo_size))
)
ReplyProfileImage(
drawableResource = LocalAccountsDataProvider
.userAccount.avatar,
description = stringResource(id = R.string.profile),
modifier = Modifier
.size(dimensionResource(R.dimen.profile_image_size))
)
}
AnimatedVisibility (showDetails) {
Text(
text = stringResource(id = LocalAccountsDataProvider
.userAccount.email),
style = MaterialTheme.typography.labelMedium,
modifier = Modifier
.padding(
start = dimensionResource(
R.dimen.drawer_padding_header),
end = dimensionResource(
R.dimen.drawer_padding_header),
bottom = dimensionResource(
R.dimen.drawer_padding_header)
),
)
}
}
}
將可展開式標頭新增至應用程式後,請執行下列步驟:
- 在測試裝置上執行應用程式
- 輕觸展開標頭
- 調整視窗大小
大幅度調整視窗大小時,你會發現標頭遺失其狀態。
之所以遺失 UI 狀態,是因為 remember
會在各次重組間保留狀態,但不會在重新建立活動或程序時保留。如要徹底避免這類問題,常見的做法是使用狀態提升,將狀態移至可組合函式的呼叫端,讓可組合函式變為無狀態。不過,你可能會使用 remember
,將 UI 元素狀態保留在可組合函式內部。
如要解決這類問題,請將 remember
替換成 rememberSaveable
。這樣做之所以可行,是因為 rememberSaveable
會儲存記下的值並還原至 savedInstanceState
。請將 remember
變更為 rememberSaveable
,在測試裝置上執行應用程式,然後再次調整應用程式大小。你會發現在調整大小時,可展開式標頭的狀態如期保留下來。
6. 避免不必要的重複背景作業
你已瞭解如何使用 rememberSaveable
,在設定發生變化 (可能會因為任意形式視窗大小調整而經常發生) 時,保留可組合函式的內部 UI 狀態。不過,應用程式應經常提升狀態,將 UI 狀態和邏輯移出可組合函式。如要在調整大小時保留狀態,最好的方法之一是將狀態擁有權遷移至 ViewModel。將狀態提升至 ViewModel
時,長時間執行的背景作業可能會造成問題,例如初始化畫面所需的大量檔案系統存取和網路呼叫作業。
如要查看你可能會遇到的問題種類示例,請在 ReplyViewModel
的 initializeUIState
方法中新增記錄陳述式。
fun initializeUIState() {
Log.d("resizing-codelab", "initializeUIState() called in the viewmodel")
val mailboxes: Map<MailboxType, List<Email>> =
LocalEmailsDataProvider.allEmails.groupBy { it.mailbox }
_uiState.value =
ReplyUiState(
mailboxes = mailboxes,
currentSelectedEmail = mailboxes[MailboxType.Inbox]?.get(0)
?: LocalEmailsDataProvider.defaultEmail
)
}
現在請在測試裝置上執行應用程式,然後調整應用程式視窗大小數次。
在 Logcat 中,你會發現應用程式顯示初始化方法已執行多次。如果你只想執行特定作業一次來初始化 UI,這可能會造成問題。額外執行的網路呼叫、檔案 I/O 或其他作業可能會影響裝置效能,並導致其他非預期問題。
如要避免不必要的背景作業,請從活動的 onCreate()
方法中移除 initializeUIState()
呼叫,改為透過 ViewModel
的 init
方法初始化資料。這可確保 ReplyViewModel
首次例項化時,初始化方法只會執行一次:
init {
initializeUIState()
}
再次執行應用程式,你會發現無論你調整應用程式視窗大小幾次,不必要的模擬初始化工作都只會執行一次。這是因為即使 Activity
的生命週期已結束,ViewModel 也會繼續存在。藉由只在 ViewModel
建立時執行初始化程式碼一次,我們就能將其與 Activity
重新建立作業分開,避免不必要的作業。如果這是為了初始化 UI 進行的高成本伺服器呼叫,或是會耗用大量資源的檔案 I/O 作業,你就能省下大量資源,同時改進使用者體驗。
7. 恭喜!
你成功了,做得好!你已採用最佳做法,讓 Android 應用程式在 ChromeOS 和其他多視窗、多螢幕環境中都能順利調整大小。
原始碼範例
從 GitHub 複製存放區
git clone https://github.com/android/large-screen-codelabs/
你也可以下載存放區的 ZIP 檔案並解壓縮