1. 簡介
在本程式碼研究室中,您將瞭解如何建構適用於手機、平板電腦和摺疊式裝置的自動調整式應用程式,以及這些應用程式如何透過 Jetpack Compose 提高可連性。您也會學到使用 Material 3 元件和主題設定的最佳做法。
深入探索前,必須先瞭解「適應能力」的意思。
適應性
應用程式的 UI 應根據不同的視窗大小、方向和板型規格做出回應。自動調整式版面配置會根據可用的畫面空間而變化。無論是為填滿空間而稍微調整版面配置、選擇相應的導覽樣式,還是為了使用額外空間而徹底變更版面配置,都屬於這類變化。
詳情請參閱「自動調整設計」。
在本程式碼研究室中,您將探索如何使用 Jetpack Compose,並思考如何靈活調整。您建構了一個名為 Reply 的應用程式,該應用程式說明如何實作適應性和可連性,進而為使用者提供最佳體驗。
課程內容
- 如何透過 Jetpack Compose 設計應用程式,指定所有視窗大小。
- 如何針對不同的折疊式裝置指定應用程式。
- 如何使用不同類型的導覽功能,提升可連性和無障礙體驗。
- 如何使用 Material 3 元件,讓各種視窗大小都能享有最佳體驗。
軟硬體需求
- 最新的 Android Studio 穩定版。
- Android 13 可調整大小的虛擬裝置。
- Kotlin 知識。
- 瞭解 Compose 的基本知識 (例如
@Composable
註解)。 - 熟悉 Compose 版面配置的基本知識 (例如
Row
和Column
)。 - 熟悉修飾符的基本知識,例如
Modifier.padding()
。
您將在本程式碼研究室中使用可調整大小的模擬器,以便在不同類型的裝置和視窗大小之間切換。
如果您不熟悉 Compose,建議您先學習 Jetpack Compose 的基本程式碼研究室,再完成本程式碼研究室。
建構項目
- 名為 Reply 的互動式電子郵件用戶端應用程式,採用最佳做法,適用於適應性設計、不同的 Material Design 導覽,以及最佳的螢幕空間使用情況。
2. 做好準備
如要取得本程式碼研究室的程式碼,請從指令列複製 GitHub 存放區:
git clone https://github.com/android/codelab-android-compose.git cd codelab-android-compose/AdaptiveUiCodelab
或者,您也可以將存放區下載為 ZIP 檔案:
建議您先從「main」分支版本的程式碼著手,依自己的步調逐步完成本程式碼研究室。
在 Android Studio 中開啟專案
- 在「Welcome to Android Studio」視窗中,選取
「Open an Existing Project」。
- 選取資料夾
<Download Location>/AdaptiveUiCodelab
(請務必選取含有「build.gradle
」的AdaptiveUiCodelab
目錄)。 - Android Studio 匯入專案後,請測試是否能執行
main
分支版本。
探索範例程式碼
main 分支版本代碼包含 ui
套件。您將在該套件中使用下列檔案:
MainActivity.kt
- 用於啟動應用程式的進入點活動。ReplyApp.kt
- 包含主畫面 UI 可組合項。ReplyHomeViewModel.kt
:提供應用程式內容的資料和 UI 狀態。ReplyListContent.kt
- 包含用於提供清單和詳細資料畫面的可組合項。
如果您在可調整大小的模擬器上執行這個應用程式,並嘗試不同的裝置類型 (例如手機或平板電腦),UI 只會展開至指定空間,而不會運用螢幕空間,也沒有提供可連性人體工學。
更新後即可充分利用螢幕空間、提高可用性,並改善整體使用者體驗。
3. 打造適合的應用程式
本節介紹如何讓應用程式具有可調整的特性,以及 Material 3 提供哪些元件來簡化這項工作。此外,它還涵蓋您要指定的螢幕和狀態類型,包括手機、平板電腦、大型平板電腦和折疊式裝置。
您會先逐一瞭解視窗大小、折疊型態以及各種導覽選項的基本知識。接著,您就可以在應用程式中使用這些 API,使應用程式更具自動調整性。
視窗大小
Android 裝置的外形與大小不盡相同,從手機、摺疊式裝置、平板電腦到 ChromeOS 裝置應有盡有。如要盡可能支援多種視窗大小,UI 必須採用回應式及自動調整式設計。為協助您找到變更應用程式 UI 的理想門檻,我們定義了中斷點值,協助將裝置分類為預先定義的大小類別 (精簡、中等和展開),稱為「視窗大小類別」。這些是一組自主的可視區域中斷點,有助於設計、開發及測試回應式與自動調整式應用程式版面配置。
這些類別經過特別挑選,目的是讓版面配置的簡潔,並靈活地根據獨特的情境來最佳化應用程式。視窗大小類別一律取決於應用程式可用的螢幕空間,但這不一定是多工處理或其他區隔作業的整個實體螢幕。
寬度和高度會分別分類,因此應用程式隨時都有兩種視窗大小類別:寬度類別和高度類別。一般來說,可用寬度比可用高度更重要,這是因為直向捲動操作比較常見,因此在這種情況下,您也會使用寬度大小類別。
折疊狀態
折疊式裝置可因應各種尺寸和轉軸,而針對更多情境做出調整。轉軸可能會遮蔽部分螢幕,導致顯示內容不適合顯示;也可以分隔,表示裝置展開時有兩個不同的實體螢幕。
此外,使用者在轉軸部分打開時看著內螢幕,因此會根據摺疊方向產生不同的實體型態:桌面型態 (上圖右側的水平摺疊,如上圖所示) 和書本型態 (垂直摺疊)。
進一步瞭解摺疊型態和轉軸。
在實作支援摺疊式裝置的自動調整式版面配置時,請將這些事項納入考量。
取得適性資訊
Material3 adaptive
程式庫可讓您輕鬆存取應用程式執行所在視窗的相關資訊。
- 將這個構件及其版本新增至版本目錄檔案:
gradle/libs.versions.toml
[versions]
material3Adaptive = "1.0.0-beta01"
[libraries]
androidx-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3Adaptive" }
- 在應用程式模組的建構檔案中,新增程式庫依附元件,然後執行 Gradle 同步處理作業:
app/build.gradle.kts
dependencies {
implementation(libs.androidx.material3.adaptive)
}
現在,在任何可組合函式範圍內,都可以使用 currentWindowAdaptiveInfo()
取得 WindowAdaptiveInfo
物件,其中包含目前的視窗大小類別,以及裝置是否處於桌面型態等摺疊式裝置型態。
你現在可以在「MainActivity
」中試用。
- 在
ReplyTheme
區塊內的onCreate()
中,取得視窗的自動調整資訊,並在Text
可組合項中顯示大小類別。您可以在ReplyApp()
元素後方加上以下內容:
MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ReplyTheme {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
ReplyApp(
replyHomeUIState = uiState,
onEmailClick = viewModel::setSelectedEmail
)
val adaptiveInfo = currentWindowAdaptiveInfo()
val sizeClassText =
"${adaptiveInfo.windowSizeClass.windowWidthSizeClass}\n" +
"${adaptiveInfo.windowSizeClass.windowHeightSizeClass}"
Text(
text = sizeClassText,
color = Color.Magenta,
modifier = Modifier.padding(20.dp)
)
}
}
}
現在執行應用程式會顯示視窗大小類別,顯示在應用程式內容上方。歡迎瞭解視窗自動調整資訊還提供的其他功能。之後你就可以移除這個Text
,因為這個專區涵蓋應用程式內容,而且後續步驟無須再執行。
4. 動態導覽
現在,請在裝置狀態和大小變更時調整應用程式的導覽功能,讓應用程式更容易使用。
使用者拿著手機時,手指通常位於畫面底部。當使用者拿著打開的摺疊式裝置或平板電腦時,手指通常會靠近兩側。使用者必須能在不使用極端手部位置或變更手部位置的情況下,就能瀏覽或啟動與應用程式的互動。
設計應用程式時,請考量螢幕不同區域的人體工學影響,並決定版面配置中該放置互動式 UI 元素的位置。
- 手持裝置時,哪些區域最舒適?
- 哪些區域只能透過延伸手指到達,哪些地方可能會比較不方便?
- 哪些地方難以觸及,或距離使用者手持裝置距離太遠?
導覽是使用者首先互動的重點,它包含與重要使用者歷程相關的重要操作,因此應放在最容易觸及的區域。Material 自動調整程式庫提供多個元件,可協助您根據裝置的視窗大小類別實作導覽。
底部導覽
底部導覽最適合精簡尺寸,因為使用者只要用拇指輕鬆按住裝置,就能輕鬆觸及底部導覽的所有接觸點。假如裝置尺寸精簡,或是處於密集折疊狀態的折疊式裝置,則適用此模式。
導覽邊欄
對於中等寬度視窗大小,導覽邊欄是確保可連性的工具,因為拇指自然會沿著裝置兩側對齊。您也可以將導覽邊欄與導覽匣結合使用,以查看詳細資訊。
導覽匣
使用導覽匣即可輕鬆查看導覽分頁的詳細資訊。使用平板電腦或大型裝置時,也能夠輕鬆存取導覽匣。系統提供兩種導覽匣:強制回應導覽匣和固定式導覽匣。
強制回應導覽匣
如果是小型到中型手機和平板電腦,您可以使用強制回應導覽匣,以疊加內容的方式展開或隱藏。有時可以與導覽邊欄合併使用。
永久性導覽匣
在大型平板電腦、Chromebook 和電腦上,你可以使用固定式導覽匣進行固定瀏覽。
實作動態導覽
現在,您將可在裝置狀態和大小變更時切換不同類型的導覽。
目前,無論裝置狀態為何,應用程式一律會在螢幕內容下方顯示 NavigationBar
。您可以改用 Material Design NavigationSuiteScaffold
元件,根據目前視窗大小類別等資訊自動切換不同的導覽元件。
- 新增 Gradle 依附元件以取得這個元件,方法是更新版本目錄和應用程式的建構指令碼,然後執行 Gradle 同步處理作業:
gradle/libs.versions.toml
[versions]
material3AdaptiveNavSuite = "1.3.0-beta01"
[libraries]
androidx-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3AdaptiveNavSuite" }
app/build.gradle.kts
dependencies {
implementation(libs.androidx.material3.adaptive.navigation.suite)
}
- 在
ReplyApp.kt
中找出ReplyNavigationWrapper()
可組合函式,然後將Column
及其內容替換為NavigationSuiteScaffold
:
ReplyApp.kt
@Composable
private fun ReplyNavigationWrapperUI(
content: @Composable () -> Unit = {}
) {
var selectedDestination: ReplyDestination by remember {
mutableStateOf(ReplyDestination.Inbox)
}
NavigationSuiteScaffold(
navigationSuiteItems = {
ReplyDestination.entries.forEach {
item(
selected = it == selectedDestination,
onClick = { /*TODO update selection*/ },
icon = {
Icon(
imageVector = it.icon,
contentDescription = stringResource(it.labelRes)
)
},
label = {
Text(text = stringResource(it.labelRes))
},
)
}
}
) {
content()
}
}
navigationSuiteItems
引數是一個區塊,可讓您使用 item()
函式新增項目,與在 LazyColumn
中新增項目類似。在結尾的 lambda 中,此程式碼會呼叫做為引數傳遞至 ReplyNavigationWrapperUI()
的 content()
。
在模擬器上執行應用程式,並嘗試在手機、摺疊式裝置和平板電腦之間變更大小,導覽列就會變更為導覽邊欄。
建議您在極寬的視窗中 (例如橫向的平板電腦) 顯示固定式導覽匣。NavigationSuiteScaffold
支援顯示固定式導覽匣,但在任何目前的 WindowWidthSizeClass
值中不會顯示。不過,您只要稍微修改即可達到這個效果。
- 請在呼叫
NavigationSuiteScaffold
之前新增下列程式碼:
ReplyApp.kt
@Composable
private fun ReplyNavigationWrapperUI(
content: @Composable () -> Unit = {}
) {
var selectedDestination: ReplyDestination by remember {
mutableStateOf(ReplyDestination.Inbox)
}
val windowSize = with(LocalDensity.current) {
currentWindowSize().toSize().toDpSize()
}
val layoutType = if (windowSize.width >= 1200.dp) {
NavigationSuiteType.NavigationDrawer
} else {
NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(
currentWindowAdaptiveInfo()
)
}
NavigationSuiteScaffold(
layoutType = layoutType,
...
) {
content()
}
}
此程式碼會先取得視窗大小,並使用 currentWindowSize()
和 LocalDensity.current
將其轉換為 DP 單位,然後比較視窗寬度來決定導覽 UI 的版面配置類型。如果視窗寬度至少為 1200.dp
,系統會使用 NavigationSuiteType.NavigationDrawer
。否則將改回預設的計算方式。
請注意,在可調整大小的模擬器上再次執行應用程式,並嘗試不同類型的裝置時,請注意,每當螢幕設定變更或展開折疊的裝置時,導覽機制都會變更為適合該大小的類型。
恭喜!您已經瞭解不同類型的導覽,可以支援不同類型的視窗大小和狀態!
在下一節中,您將瞭解如何善用剩餘的螢幕區域,而不是將同一個清單項目邊緣延展至邊緣。
5. 螢幕空間使用情形
無論您是在小型平板電腦、展開狀態的裝置或大型平板電腦中執行應用程式,畫面都會延展至填滿剩餘空間。您想確實利用這個螢幕空間顯示更多資訊 (比如這個應用程式),使用者會在同一個頁面看到電子郵件和討論串。
Material 3 定義了三種標準版面配置,每個版面配置各有精簡、中等和展開視窗大小類別的設定。「清單詳細資料」標準版面配置非常適合此用途,並且以 ListDetailPaneScaffold
的形式在 Compose 中使用。
- 新增下列依附元件並執行 Gradle 同步處理作業即可取得這個元件:
gradle/libs.versions.toml
[libraries]
androidx-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "material3Adaptive" }
androidx-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "material3Adaptive" }
app/build.gradle.kts
dependencies {
implementation(libs.androidx.material3.adaptive.layout)
implementation(libs.androidx.material3.adaptive.navigation)
}
- 在
ReplyApp.kt
中找出ReplyAppContent()
可組合函式,該函式目前只會透過呼叫ReplyListPane()
顯示清單窗格。請插入下列程式碼,以將實作項目替換為ListDetailPaneScaffold
。由於這是實驗性 API,因此您也需要在ReplyAppContent()
函式中新增@OptIn
註解:
ReplyApp.kt
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ReplyAppContent(
replyHomeUIState: ReplyHomeUIState,
onEmailClick: (Email) -> Unit,
) {
val navigator = rememberListDetailPaneScaffoldNavigator<Long>()
ListDetailPaneScaffold(
directive = navigator.scaffoldDirective,
value = navigator.scaffoldValue,
listPane = {
ReplyListPane(replyHomeUIState, onEmailClick)
},
detailPane = {
ReplyDetailPane(replyHomeUIState.emails.first())
}
)
}
此程式碼會先使用 rememberListDetailPaneNavigator
建立導覽器。導覽器提供一些控制項,讓您控管要顯示的窗格,以及該窗格應呈現的內容,稍後將進行示範。
展開視窗寬度大小類別時,ListDetailPaneScaffold
會顯示兩個窗格。否則,應用程式會根據為兩個參數提供的值 (scaffold 指令和 Scaffold 值) 顯示一個窗格或另一個窗格。為了取得預設行為,此程式碼會使用導覽器提供的 scaffold 指令和 Scaffold 值。
其餘的必要參數是窗格的可組合 lambda。ReplyListPane()
和 ReplyDetailPane()
(位於 ReplyListContent.kt
) 會分別填入清單窗格和詳細資料窗格的角色。ReplyDetailPane()
需要電子郵件引數,因此這個程式碼目前使用 ReplyHomeUIState
電子郵件清單中的第一封電子郵件。
執行應用程式,並將模擬器檢視畫面切換為折疊式或平板電腦 (可能也需要變更螢幕方向),才能看到兩個窗格的版面配置。這看起來比以往好多了!
現在我們來看看此畫面的一些期望行為。當使用者輕觸清單窗格中的電子郵件時,該郵件與所有回覆都會顯示在詳細資料窗格中。應用程式目前不會記錄選取的電子郵件,使用者輕觸項目則不會執行任何動作。保留這項資訊的最佳位置,就是 ReplyHomeUIState
中的其餘 UI 狀態。
- 開啟
ReplyHomeViewModel.kt
並找到ReplyHomeUIState
資料類別。為所選電子郵件新增屬性,預設值為null
:
ReplyHomeViewModel.kt
data class ReplyHomeUIState(
val emails : List<Email> = emptyList(),
val selectedEmail: Email? = null,
val loading: Boolean = false,
val error: String? = null
)
- 在同一個檔案中,
ReplyHomeViewModel
具有使用者輕觸清單項目時會呼叫的setSelectedEmail()
函式。修改這個函式來複製 UI 狀態並記錄所選電子郵件:
ReplyHomeViewModel.kt
fun setSelectedEmail(email: Email) {
_uiState.update {
it.copy(selectedEmail = email)
}
}
使用者輕觸任何項目且選取的電子郵件為 null
前的後果,請特別留意。詳細資料窗格中應顯示什麼?處理這種情況的方法有很多種,例如預設顯示清單中的第一個項目。
- 在同一個檔案中,修改
observeEmails()
函式。載入電子郵件清單後,如果先前的 UI 狀態尚未選取電子郵件,請將其設為第一個項目:
ReplyHomeViewModel.kt
private fun observeEmails() {
viewModelScope.launch {
emailsRepository.getAllEmails()
.catch { ex ->
_uiState.value = ReplyHomeUIState(error = ex.message)
}
.collect { emails ->
val currentSelection = _uiState.value.selectedEmail
_uiState.value = ReplyHomeUIState(
emails = emails,
selectedEmail = currentSelection ?: emails.first()
)
}
}
}
- 返回
ReplyApp.kt
,然後使用所選電子郵件 (如果有的話) 填入詳細資料窗格內容:
ReplyApp.kt
ListDetailPaneScaffold(
// ...
detailPane = {
if (replyHomeUIState.selectedEmail != null) {
ReplyDetailPane(replyHomeUIState.selectedEmail)
}
}
)
再次執行應用程式,並將模擬器切換為平板電腦大小,然後查看輕觸清單項目會更新詳細資料窗格的內容。
當兩個窗格都顯示時,這項功能就能正常運作,但當視窗空間只能顯示一個窗格時,使用者輕觸項目並不會發生任何變化。請嘗試將模擬器檢視畫面切換到手機或直向的摺疊式裝置。請注意,即使輕觸項目,畫面上還是只會顯示清單窗格。這是因為即使選取的電子郵件已更新,ListDetailPaneScaffold
仍會專注於這些設定中的清單窗格。
- 如要修正這個問題,請插入下列程式碼做為傳遞至
ReplyListPane
的 lambda:
ReplyApp.kt
ListDetailPaneScaffold(
// ...
listPane = {
ReplyListPane(
replyHomeUIState = replyHomeUIState,
onEmailClick = { email ->
onEmailClick(email)
navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
}
)
},
// ...
)
這個 lambda 會使用先前建立的導覽器,在點選項目時新增其他行為。它會呼叫傳遞至這個函式的原始 lambda,然後呼叫 navigator.navigateTo()
來指定要顯示的窗格。Scaffold 中的每個窗格都有與其相關聯的角色,詳細資料窗格中則為 ListDetailPaneScaffoldRole.Detail
。在較小的視窗中,這會顯示應用程式往後瀏覽的外觀。
當使用者按下詳細資料窗格中的返回按鈕時,應用程式也必須處理後續影響,視有顯示一個窗格或兩個窗格而定,此行為會有所不同。
- 新增下列程式碼即可支援返回導覽功能。
ReplyApp.kt
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ReplyAppContent(
replyHomeUIState: ReplyHomeUIState,
onEmailClick: (Email) -> Unit,
) {
val navigator = rememberListDetailPaneScaffoldNavigator<Long>()
BackHandler(navigator.canNavigateBack()) {
navigator.navigateBack()
}
ListDetailPaneScaffold(
directive = navigator.scaffoldDirective,
value = navigator.scaffoldValue,
listPane = {
AnimatedPane {
ReplyListPane(
replyHomeUIState = replyHomeUIState,
onEmailClick = { email ->
onEmailClick(email)
navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
}
)
}
},
detailPane = {
AnimatedPane {
if (replyHomeUIState.selectedEmail != null) {
ReplyDetailPane(replyHomeUIState.selectedEmail)
}
}
}
)
}
導覽工具知道 ListDetailPaneScaffold
的完整狀態、是否提供返回導覽功能,以及所有這些情境的處理方式。此程式碼會建立 BackHandler
,每當導覽器可返回瀏覽,且在 lambda 中呼叫 navigateBack()
時,系統都會啟用該功能。此外,為了讓窗格之間的轉換更順暢,每個窗格都會納入 AnimatedPane()
可組合項中。
在可調整大小的模擬器上,針對所有不同類型的裝置再次執行應用程式,您會發現每當螢幕設定變更,或展開摺疊式裝置時,導覽和畫面內容都會隨著裝置狀態變更而動態變更。也可以嘗試輕觸清單窗格中的電子郵件,看看版面配置在不同畫面中的行為、並排顯示兩個窗格,或是在兩個窗格之間流暢顯示動畫。
恭喜!您已成功讓應用程式針對各種裝置狀態和尺寸進行調整。然後開始在折疊式裝置、平板電腦或其他行動裝置上執行應用程式。
6. 恭喜
恭喜!您已經成功完成本程式碼研究室,並瞭解如何使用 Jetpack Compose 讓應用程式自動調整。
您已瞭解如何檢查裝置的大小和折疊狀態,並據此更新應用程式的使用者介面、導覽和其他功能。此外,您也學到適應性如何提升可連性並改善使用者體驗。
後續步驟
請參閱 Compose 課程中的其他程式碼研究室。
範例應用程式
- Compose 範例有許多應用程式,這些應用程式都整合了程式碼研究室說明的最佳做法。