使用 Jetpack Compose 建構自動調整式應用程式

1. 簡介

在本程式碼研究室中,您將瞭解如何建構適用於手機、平板電腦和摺疊式裝置的自動調整式應用程式,以及這些應用程式如何透過 Jetpack Compose 提高可連性。您也會學到使用 Material 3 元件和主題設定的最佳做法。

深入探索前,必須先瞭解「適應能力」的意思。

適應性

應用程式的 UI 應根據不同的視窗大小、方向和板型規格做出回應。自動調整式版面配置會根據可用的畫面空間而變化。無論是為填滿空間而稍微調整版面配置、選擇相應的導覽樣式,還是為了使用額外空間而徹底變更版面配置,都屬於這類變化。

詳情請參閱「自動調整設計」。

在本程式碼研究室中,您將探索如何使用 Jetpack Compose,並思考如何靈活調整。您建構了一個名為 Reply 的應用程式,該應用程式說明如何實作適應性和可連性,進而為使用者提供最佳體驗。

課程內容

  • 如何透過 Jetpack Compose 設計應用程式,指定所有視窗大小。
  • 如何針對不同的折疊式裝置指定應用程式。
  • 如何使用不同類型的導覽功能,提升可連性和無障礙體驗。
  • 如何使用 Material 3 元件,讓各種視窗大小都能享有最佳體驗。

事前準備

您將在本程式碼研究室中使用可調整大小的模擬器,以便在不同類型的裝置和視窗大小之間切換。

可調整大小的模擬器,提供手機、展開狀態、平板電腦和桌上型電腦的選項。

如果您不熟悉 Compose,建議您先學習 Jetpack Compose 的基本程式碼研究室,再完成本程式碼研究室。

建構項目

  • 一種互動式電子郵件用戶端應用程式,採用最佳做法,適用於適應性設計、不同的 Material Design 導覽,以及最佳的螢幕空間用量。

您將在本程式碼研究室中達成多款裝置支援

2. 做好準備

如要取得本程式碼研究室的程式碼,請從指令列複製 GitHub 存放區:

git clone https://github.com/android/codelab-android-compose.git
cd codelab-android-compose/AdaptiveUiCodelab

或者,您也可以將存放區下載為 ZIP 檔案:

建議您先從「main」分支版本的程式碼著手,依自己的步調逐步完成本程式碼研究室。

在 Android Studio 中開啟專案

  1. 在「Welcome to Android Studio」視窗中,選取 c01826594f360d94.png「Open an Existing Project」
  2. 選取資料夾 <Download Location>/AdaptiveUiCodelab (請務必選取含有「build.gradleAdaptiveUiCodelab 目錄)。
  3. Android Studio 匯入專案後,請測試是否能執行 main 分支版本。

探索範例程式碼

main 分支版本代碼包含 ui 套件。您將在該套件中使用下列檔案:

  • MainActivity.kt - 用於啟動應用程式的進入點活動。
  • ReplyApp.kt - 包含主畫面 UI 可組合項。
  • ReplyHomeViewModel.kt:提供應用程式內容的資料和 UI 狀態。
  • ReplyListContent.kt - 包含用於提供清單和詳細資料畫面的可組合項。

您必須先將焦點移至「MainActivity.kt

MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent {
        ReplyTheme {
            val uiState by viewModel.uiState.collectAsStateWithLifecycle()
            ReplyApp(
                replyHomeUIState = uiState,
                onEmailClick = viewModel::setSelectedEmail
            )
        }
    }
}

如果您在可調整大小的模擬器上執行這個應用程式,並嘗試不同的裝置類型 (例如手機或平板電腦),UI 只會展開至指定空間,而不會運用螢幕空間,也沒有提供可連性人體工學。

手機的初始畫面

平板電腦上初始延展檢視畫面

更新後即可充分利用螢幕空間、提高可用性,並改善整體使用者體驗。

3. 讓應用程式適用

本節介紹如何讓應用程式具有可調整的特性,以及 Material 3 提供哪些元件來簡化這項工作。此外,它還涵蓋您要指定的螢幕和狀態類型,包括手機、平板電腦、大型平板電腦和折疊式裝置。

您會先逐一瞭解視窗大小、折疊型態以及各種導覽選項的基本知識。接著,您就可以在應用程式中使用這些 API,使應用程式更具自動調整性。

視窗大小

Android 裝置的外形與大小不盡相同,從手機、摺疊式裝置、平板電腦到 ChromeOS 裝置應有盡有。如要盡可能支援多種視窗大小,UI 必須採用回應式及自動調整式設計。為協助您找到變更應用程式 UI 的理想門檻,我們定義了中斷點值,協助將裝置分類為預先定義的大小類別 (精簡、中等和展開),稱為「視窗大小類別」。這些是一組自主的可視區域中斷點,有助於設計、開發及測試回應式與自動調整式應用程式版面配置。

這些類別經過特別挑選,目的是讓版面配置的簡潔,並靈活地根據獨特的情境來最佳化應用程式。視窗大小類別一律取決於應用程式可用的螢幕空間,但這不一定是多工處理或其他區隔作業的整個實體螢幕。

適用於精簡、中等和展開寬度的 WindowWidthSizeClass。

適用於精簡、中等和展開高度的 WindowHeightSizeClass。

寬度和高度會分別分類,因此應用程式隨時都有兩種視窗大小類別:寬度類別和高度類別。一般來說,可用寬度比可用高度更重要,這是因為直向捲動操作比較常見,因此在這種情況下,您也會使用寬度大小類別。

折疊狀態

折疊式裝置可因應各種尺寸和轉軸,而針對更多情境做出調整。轉軸可能會遮蔽部分螢幕,導致該區域不適合顯示內容;同時也可能遭到分隔,亦即裝置展開時有兩個不同的實體螢幕。

折疊型態、平鋪式和半開式

此外,使用者在轉軸部分打開時看著內螢幕,因此會根據摺疊方向產生不同的實體型態:桌面型態 (上圖右側的水平摺疊,如上圖所示) 和書本型態 (垂直摺疊)。

進一步瞭解摺疊型態和轉軸

在實作支援摺疊式裝置的自動調整式版面配置時,請將這些事項納入考量。

取得適性資訊

Material3 adaptive 程式庫可讓您輕鬆存取應用程式執行所在視窗的相關資訊。

  1. 將這個構件及其版本新增至版本目錄檔案:

gradle/libs.versions.toml

[versions]
material3Adaptive = "1.0.0-beta01"

[libraries]
androidx-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3Adaptive" }
  1. 在應用程式模組的建構檔案中,新增程式庫依附元件,然後執行 Gradle 同步處理作業:

app/build.gradle.kts

dependencies {

    implementation(libs.androidx.material3.adaptive)
}

現在,在任何可組合函式範圍內,都可以使用 currentWindowAdaptiveInfo() 取得 WindowAdaptiveInfo 物件,其中包含目前的視窗大小類別,以及裝置是否處於桌面型態等摺疊式裝置型態。

你現在可以在「MainActivity」中試用。

  1. 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 Design 提供多種元件,可協助您根據裝置的視窗大小類別實作導覽功能。

底部導覽

底部導覽最適合精簡尺寸,因為使用者只要用拇指輕鬆按住裝置,就能輕鬆觸及底部導覽的所有接觸點。假如裝置尺寸精簡,或是處於密集折疊狀態的折疊式裝置,則適用此模式。

含有項目的底部導覽列

對於中等寬度視窗大小,導覽邊欄是確保可連性的工具,因為拇指自然會沿著裝置兩側對齊。您也可以將導覽邊欄與導覽匣結合使用,以查看詳細資訊。

含有項目的導覽邊欄

使用導覽匣即可輕鬆查看導覽分頁的詳細資訊。使用平板電腦或大型裝置時,也能夠輕鬆存取導覽匣。系統提供兩種導覽匣:強制回應導覽匣和固定式導覽匣。

強制回應導覽匣

如果是小型到中型手機和平板電腦,您可以使用強制回應導覽匣,以疊加內容的方式展開或隱藏。有時可以與導覽邊欄合併使用。

含有項目的強制回應導覽匣

永久性導覽匣

在大型平板電腦、Chromebook 和電腦上,你可以使用固定式導覽匣進行固定瀏覽。

有項目的永久性導覽匣

實作動態導覽

現在,您將可在裝置狀態和大小變更時切換不同類型的導覽。

目前,無論裝置狀態為何,應用程式一律會在螢幕內容下方顯示 NavigationBar。您可以改用 Material Design NavigationSuiteScaffold 元件,根據目前視窗大小類別等資訊自動切換不同的導覽元件。

  1. 新增 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)
}
  1. 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 值中不會顯示。不過,您只要稍微修改即可達到這個效果。

  1. 請在呼叫 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 中使用。

  1. 新增下列依附元件並執行 Gradle 同步處理作業即可取得這個元件:

gradle/libs.versions.toml

[versions]
material3Adaptive = "1.0.0-beta01"

[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)
}
  1. 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 狀態。

  1. 開啟 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
)
  1. 在同一個檔案中,ReplyHomeViewModel 具有使用者輕觸清單項目時會呼叫的 setSelectedEmail() 函式。修改這個函式來複製 UI 狀態並記錄所選電子郵件:

ReplyHomeViewModel.kt

fun setSelectedEmail(email: Email) {
    _uiState.update {
        it.copy(selectedEmail = email)
    }
}

使用者輕觸任何項目且選取的電子郵件為 null 前的後果,請特別留意。詳細資料窗格中應顯示什麼?處理這種情況的方法有很多種,例如預設顯示清單中的第一個項目。

  1. 在同一個檔案中,修改 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()
                )
            }
    }
}
  1. 返回 ReplyApp.kt,然後使用所選電子郵件 (如果有的話) 填入詳細資料窗格內容:

ReplyApp.kt

ListDetailPaneScaffold(
    // ...
    detailPane = {
        if (replyHomeUIState.selectedEmail != null) {
            ReplyDetailPane(replyHomeUIState.selectedEmail)
        }
    }
)

再次執行應用程式,並將模擬器切換為平板電腦大小,然後查看輕觸清單項目會更新詳細資料窗格的內容。

當兩個窗格都顯示時,這項功能就能正常運作,但當視窗空間只能顯示一個窗格時,使用者輕觸項目並不會發生任何變化。請嘗試將模擬器檢視畫面切換到手機或直向的摺疊式裝置。請注意,即使輕觸項目,畫面上還是只會顯示清單窗格。這是因為即使選取的電子郵件已更新,ListDetailPaneScaffold 仍會專注於這些設定中的清單窗格。

  1. 如要修正這個問題,請插入下列程式碼做為傳遞至 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。在較小的視窗中,這會顯示應用程式往後瀏覽的外觀。

當使用者按下詳細資料窗格中的返回按鈕時,應用程式也必須處理後續影響,視有顯示一個窗格或兩個窗格而定,此行為會有所不同。

  1. 新增下列程式碼即可支援返回導覽功能。

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 範例有許多應用程式,這些應用程式都採用了程式碼研究室說明的最佳做法。

參考文件