使用動態導覽建構自動調整式應用程式

1. 簡介

在 Android 平台中開發應用程式的主要優點之一,就是能廣泛觸及各種板型規格的使用者,例如穿戴式裝置、折疊式裝置、平板電腦、桌上型電腦,甚至電視等。使用應用程式時,使用者可能會希望在大螢幕裝置中也可以使用相同的應用程式,以便充分運用可用空間。有越來越多的 Android 使用者會在不同螢幕尺寸的裝置中使用應用程式,並希望在所有裝置中都能享有高品質的使用者體驗。

到目前為止,您已經瞭解如何打造適合行動裝置使用的應用程式。在本程式碼研究室中,您將瞭解如何轉換應用程式,讓應用程式可以依照不同螢幕大小調整使用。您將使用可自動調整的導覽版面配置模式,這種模式不但美觀,而且在行動裝置和大螢幕裝置 (例如:折疊式裝置、平板電腦和桌上型電腦) 中都可使用。

必要條件

  • 熟悉 Kotlin 程式設計,包含類別、函式和條件式
  • 熟悉如何使用 ViewModel 類別
  • 熟悉如何建立 Composables 函式
  • 瞭解如何使用 Jetpack Compose 建構版面配置
  • 瞭解如何使用裝置或模擬器執行應用程式

課程內容

  • 如何在沒有導覽圖的情況下為簡易應用程式建立畫面之間的導覽
  • 如何使用 Jetpack Compose 建立自動調整導覽版面配置
  • 如何建立自訂返回處理常式

建構項目

  • 您將在現有的 Reply 應用程式中實作動態導覽,將其版面配置調整為適合所有螢幕大小

完成的作品應如下圖所示:

​​ 在本程式碼研究室的結尾將顯示 Reply 應用程式的插圖,並在左側顯示導覽匣。導覽匣會列出 4 個可供使用者選取的分頁標籤:「Inbox」、「Sent」、「Drafts」和「Spam」。導覽匣右側則會顯示電子郵件範例清單。

軟硬體需求

  • 可連接網際網路且有網頁瀏覽器的電腦、Android Studio
  • GitHub 存取權

2. 應用程式總覽

Reply 應用程式簡介

Reply 應用程式是功能類似電子郵件用戶端的多螢幕應用程式。

Reply 應用程式會在手機模式中顯示。應用程式會顯示電子郵件範例清單,供使用者閱讀。畫面底部有四個圖示,分別代表收件匣、寄件備份、草稿和垃圾郵件。

其中包含 4 種不同的類別,而且會以不同的分頁應用程式中包含 4 種不同的類別,而且會以不同的分頁標籤顯示,分別是:收件匣、寄件備份、草稿和垃圾郵件。標籤顯示,分別是:收件匣、寄件備份、草稿和垃圾郵件。

下載範例程式碼

在 Android Studio 中開啟 basic-android-kotlin-compose-training-reply-app 資料夾。

  1. 前往為專案提供的 GitHub 存放區頁面。
  2. 驗證分支版本名稱與程式碼研究室中指定的分支版本名稱相符。例如,在下方螢幕截圖中,分支版本名稱為「main」。

1e4c0d2c081a8fd2.png

  1. 在專案的 GitHub 頁面中,按一下「Code」按鈕,畫面就會顯示彈出式視窗。

1debcf330fd04c7b.png

  1. 在彈出式視窗中,按一下「Download ZIP」(下載 ZIP) 按鈕,將專案儲存至電腦。等待下載作業完成。
  2. 在電腦中找到該檔案 (可能位於「下載」資料夾中)。
  3. 按兩下 ZIP 檔案,將檔案解壓縮。這項操作會建立含有專案檔案的新資料夾。

在 Android Studio 中開啟專案

  1. 啟動 Android Studio。
  2. 在「Welcome to Android Studio」(歡迎使用 Android Studio) 視窗中,按一下「Open」(開啟)

d8e9dbdeafe9038a.png

注意:如果已開啟 Android Studio,請依序選取「File」(檔案) >「Open」(開啟) 選單選項。

8d1fda7396afe8e5.png

  1. 在檔案瀏覽器中,前往已解壓縮的專案資料夾所在的位置 (可能位於「下載」資料夾中)。
  2. 按兩下該專案資料夾。
  3. 等待 Android Studio 開啟專案。
  4. 按一下「Run」按鈕 8de56cba7583251f.png 即可建構並執行應用程式。請確認應用程式的建構符合預期。

3. 範例程式碼逐步操作說明

Reply 應用程式中的重要目錄

Reply 應用程式檔案目錄會顯示兩個已展開的子目錄:「data」和「ui」。在 ui 目錄中,已選取 MainActivity.kt。MainActivity.kt 會出現在其內容清單的結尾處。

Reply 應用程式專案的資料和使用者介面層會分為不同的目錄。ReplyViewModelReplyUiState 和其他可組合項位於 ui 目錄中。定義資料層和資料供應商類別的 dataenum 類別位於 data 目錄中。

Reply 應用程式中的資料初始化

系統會透過 ReplyViewModel 中的 initilizeUIState() 方法 (在 init 函式中執行),使用相關資料來初始化 Reply 應用程式。

ReplyViewModel.kt

...
   init {
        initializeUIState()
    }

   private fun initializeUIState() {
        var mailboxes: Map<MailboxType, List<Email>> =
            LocalEmailsDataProvider.allEmails.groupBy { it.mailbox }
        _uiState.value =
            ReplyUiState(
                mailboxes = mailboxes,
                currentSelectedEmail = mailboxes[MailboxType.Inbox]?.get(0)
                    ?: LocalEmailsDataProvider.defaultEmail
            )
    }
...

畫面層級可組合項

與其他應用程式一樣,Reply 應用程式會使用 ReplyApp 可組合項做為主要的可組合項,並於其中宣告 viewModeluiState。不同的 viewModel 函式也會做為 ReplyHomeScreen 可組合項的 lambda 引數傳遞。

ReplyApp.kt

...
@Composable
fun ReplyApp(modifier: Modifier = Modifier) {
    val viewModel: ReplyViewModel = viewModel()
    val replyUiState = viewModel.uiState.collectAsState().value

    ReplyHomeScreen(
        replyUiState = replyUiState,
        onTabPressed = { mailboxType: MailboxType ->
            viewModel.updateCurrentMailbox(mailboxType = mailboxType)
            viewModel.resetHomeScreenStates()
        },
        onEmailCardPressed = { email: Email ->
            viewModel.updateDetailsScreenStates(
                email = email
            )
        },
        onDetailScreenBackPressed = {
            viewModel.resetHomeScreenStates()
        },
        modifier = modifier
    )
}

其他可組合項

  • ReplyHomeScreen.kt 包含主畫面的畫面可組合項,包括導覽元素在內。
  • ReplyHomeContent.kt 包含可組合項,這些可定義更詳細的主畫面可組合項。
  • ReplyDetailsScreen.kt 包含畫面可組合項及較小的細節畫面可組合項。

您可以細讀每個檔案以進一步瞭解可組合項,然後再繼續程式碼研究室接下來的章節。

4. 變更沒有導覽圖的畫面

在先前的課程中,您已瞭解如何使用 NavHostController 類別在不同畫面之間導覽。透過 Compose,您也可以利用執行階段可變動狀態,使用簡易條件陳述式變更畫面。這對於小型應用程式 (例如只要在兩個畫面之間切換的 Reply 應用程式) 而言特別實用。

變更有狀態異動的畫面

在 Compose 中,當狀態有所異動時,系統就會重組畫面。您可以使用簡單的條件式來變更畫面,藉此回應狀態異動。

當使用者位於主畫面時,您將使用條件式顯示主畫面的內容,然後在使用者離開主畫面時顯示詳細資料畫面。

完成下列步驟,即可修改 Reply 應用程式以允許狀態異動時的畫面變更:

  1. 在 Android Studio 中開啟範例程式碼。
  2. ReplyHomeScreen.ktReplyHomeScreen 可組合項中,當 replyUiState 物件的 isShowingHomepage 屬性為 true 時,使用 if 陳述式納入 ReplyAppContent 可組合項。

ReplyHomeScreen.kt

@Composable
fun ReplyHomeScreen(
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit = {},
    onEmailCardPressed: (Int) -> Unit = {},
    onDetailScreenBackPressed: () -> Unit = {},
    modifier: Modifier = Modifier
) {

...
    if (replyUiState.isShowingHomepage) {
        ReplyAppContent(
            replyUiState = replyUiState,
            onTabPressed = onTabPressed,
            onEmailCardPressed = onEmailCardPressed,
            navigationItemContentList = navigationItemContentList,
            modifier = modifier

        )
    }
}

您現在必須以顯示詳細資料畫面的方式,將使用者離開主畫面時的情況納入考量。

  1. 新增在主體中已經含有 ReplyDetailsScreen 可組合項的 else 分支版本。新增 replyUIStateonDetailScreenBackPressedmodifier 做為 ReplyDetailsScreen 可組合項的引數。

ReplyHomeScreen.kt

@Composable
fun ReplyHomeScreen(
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit = {},
    onEmailCardPressed: (Int) -> Unit = {},
    onDetailScreenBackPressed: () -> Unit = {},
    modifier: Modifier = Modifier
) {

...

    if (replyUiState.isShowingHomepage) {
        ReplyAppContent(
            replyUiState = replyUiState,
            onTabPressed = onTabPressed,
            onEmailCardPressed = onEmailCardPressed,
            navigationItemContentList = navigationItemContentList,
            modifier = modifier

        )
    } else {
        ReplyDetailsScreen(
            replyUiState = replyUiState,
            onBackPressed = onDetailScreenBackPressed,
            modifier = modifier
        )
    }
}

replyUiState 物件是狀態物件。因此,如果 replyUiState 物件的 isShowingHomepage 屬性有變更,在執行階段時,ReplyHomeScreen 可組合項就會重組,且 if/else 陳述式也會重新評估。此方法不需要使用 NavHostController 類別,即可支援不同畫面之間的導覽。

手機模擬器中的 Reply 應用程式插圖動畫,顯示從主畫面到詳細資料頁面的畫面。主畫面會顯示電子郵件清單,並在底部顯示 4 個訊息的圖示 (收件匣、寄件備份、草稿和垃圾郵件)。詳細資料頁面會顯示範例電子郵件的全文,並於下方顯示「回覆」和「回覆所有人」的按鈕。

建立自訂返回處理常式

使用 NavHost 可組合項在畫面之間切換的其中一個好處,就是可以將上一個畫面的方向儲存在返回堆疊中。這些已儲存的畫面可讓系統返回按鈕在叫用時輕鬆導覽至上一個畫面。由於 Reply 應用程式沒有使用 NavHost,因此您必須新增程式碼才能手動處理返回按鈕。這是您接下來要完成的工作。

如要在 Reply 應用程式中建立自訂返回處理常式,請完成下列步驟:

  1. ReplyDetailsScreen 可組合項的第一行中新增 BackHandler 可組合項。
  2. BackHandler 可組合項主體中呼叫 onBackPressed() 函式。

ReplyDetailsScreen.kt

...
import androidx.activity.compose.BackHandler
...
@Composable
fun ReplyDetailsScreen(
    replyUiState: ReplyUiState,
    modifier: Modifier = Modifier,
    onBackPressed: () -> Unit = {},
) {
    BackHandler {
        onBackPressed()
    }
...

5. 在大螢幕裝置中執行應用程式

使用可調整大小的模擬器檢查應用程式

為了製作可用應用程式,開發人員必須瞭解各種板型規格的使用者體驗。因此,您必須在開發程序一開始就針對各種板型規格測試應用程式。

您可以使用不同螢幕尺寸的模擬器來達成這個目標。但是這樣做可能會相當麻煩,尤其如果要同時針對多種螢幕大小建構時更是如此。您可能也需要測試執行中的應用程式如何依照螢幕大小變更,例如螢幕方向變更、桌上型電腦中的視窗大小變更,以及折疊式裝置的折疊狀態變更。

Android Studio 推出了可調整大小的模擬器,可協助您針對這些不同的情況進行測試。

如要設定可調整大小的模擬器,請完成下列步驟:

  1. 確認您使用的是 Android Studio Chipmunk | 2021.2.1 以上版本。
  2. 在 Android Studio 中,依序選取「Tools」>「Device Manager」

「Tools」選單會顯示選項清單。選取在清單下半部顯示的「Device Manager」。

  1. 在「Device Manager」中,按一下「Create device」裝置管理工具的工具列中會顯示兩個選單選項:「Virtual」和「Physical」。在這些選項下方,有一個「Create Device」按鈕。
  2. 依序選取「Phone」類別和「Resizable (Experimental)」裝置。
  3. 按一下「Next」

「Device Manager」視窗會顯示選擇裝置定義的通知。選項清單上方會顯示搜尋欄位。選取「Phone」類別,然後選取裝置定義名稱「Resizable (Experimental)」。

  1. 選取「API Level 33」
  2. 按一下「Next」

「Virtual Device Configuration」視窗會顯示選取系統映像檔的提示。選取「Tiramisu API」。

  1. 為新的 Android 虛擬裝置命名。
  2. 按一下「Finish」。

Android 虛擬裝置 (AVD) 中的「Virtual Configation」畫面即會顯示。設定畫面包含輸入 AVD 名稱使用的文字欄位。名稱欄位下方會列出裝置選項,包括裝置定義 (可調整大小,實驗功能)、系統映像檔 (Tiramisu) 和螢幕方向 (預設選取的螢幕方向為直向)。「Change」按鈕會顯示在裝置定義和系統映像檔資訊的右側,「Landscape」選項則位於所選「Portrait」選項的右側。右下角有 4 個按鈕:「Cancel」、「Previous」、「Next」(顯示為灰色且無法選取) 和「Finish」。

在大螢幕模擬器中執行應用程式

現在您已設定可調整大小的模擬器,接下來我們要來看看應用程式在大螢幕中的呈現效果。

  1. 在可調整大小的模擬器中執行應用程式。
  2. 選取「Tablet」做為顯示模式。

可調整大小的模擬器在手機螢幕中顯示 Reply 應用程式。應用程式會顯示訊息清單,並在畫面底部顯示 4 個圖示,分別是「收件匣」、「寄件備份」、「草稿」和「垃圾郵件」。

  1. 在平板電腦模式中,以橫向模式檢查應用程式。

可調整大小的模擬器顯示在平板電腦螢幕中執行的 Reply 應用程式,且主體已經拉長。畫面底部會顯示收件匣、寄件備份、草稿和垃圾郵件的圖示。

您會發現平板電腦螢幕顯示的畫面朝水平方向拉長。雖然此螢幕方向的功能運作正常,但可能不適合大螢幕裝置使用。所以接下來我們要解決這個問題。

針對大螢幕設計

在平板電腦中查看此應用程式時,您的第一個想法會是這個應用程式不但設計不良,而且缺乏吸引力。沒錯,此版面配置「並非」為大螢幕使用而設計

為大螢幕 (例如平板電腦和折疊式裝置) 使用體驗設計時,您必須考量使用者人體工學,以及使用者手指與螢幕的距離。使用行動裝置時,使用者可以使用手指輕鬆存取畫面中的多數內容,而且互動元素 (例如按鈕和導覽元素) 的位置也不會帶來太大影響。但使用大型螢幕時,畫面中央如果有重要的互動元素,使用時可能會較為困難。

就像我們在 Reply 應用程式中看到的一樣,大螢幕體驗設計並非單純只是為了配合螢幕大小而延展或放大 UI 元素。您必須善加利用更大的可用空間,為使用者打造出不同的使用體驗。舉例來說,您可以在同一個畫面中加入其他版面配置,這樣就不必導覽至另一個畫面,或是必須進行多工處理。

Reply 應用程式的詳細資料畫面會在首頁底部顯示,同時也會顯示導覽匣和電子郵件清單。範例電子郵件會顯示在電子郵件清單的右側。「Reply」和「Reply All」按鈕會在電子郵件範例下方顯示。

此設計不但可以提升使用者的工作效率,還可以進一步提高參與度。但是在部署此設計之前,您必須先瞭解如何為不同螢幕大小建立不同的版面配置。

6. 配合不同螢幕大小調整版面配置

什麼是中斷點?

您也許會想知道同一個應用程式要如何才能顯示不同的版面配置。簡單來說,就是針對不同的狀態使用條件式,和本程式碼研究室開頭所說的方式一樣。

如要建立可自動調整的應用程式,您必須根據螢幕大小變更版面配置。版面配置變更的測量點稱為中斷點。質感設計建立了一個可自主設計的中斷點範圍,適合大多數的 Android 螢幕使用。

表格中顯示不同裝置類型和設定的中斷點範圍 (以 dp 為單位)。0 到 599 dp 適用於直向模式的手機、橫向顯示的手機、精簡的視窗大小、4 欄以及 8 個最小邊界。600 到 839 dp 適用於直向或橫向模式的折疊式小型平板電腦、中型視窗大小類別、12 欄以及 12 個最小邊界。840 dp 以上適用於直向或橫向模式的大型平板電腦、展開的視窗大小類別、12 欄以及 32 個最小邊界。表格附註說明邊界和溝槽可彈性調整,且大小不必相等,而且橫向的手機視為例外狀況,並仍符合 0 到 599 dp 的中斷點範圍之內。

舉例來說,根據此中斷點範圍表,如果您的應用程式目前是在螢幕大小小於 600 dp 的裝置中執行,就應顯示行動裝置版面配置。

使用視窗大小類別

針對 Compose 加入的 WindowSizeClass API,可簡化質感設計中斷點的實作。

視窗大小類別有三種大小類別:精簡、中型和展開 (寬度和高度)。

此圖表代表以寬度為基礎的視窗大小類別。 此圖表代表以高度為基礎的視窗大小類別。

如要在 Reply 應用程式中實作 WindowSizeClass API,請完成下列步驟:

  1. 新增 material3-window-size-class 依附元件至模組 build.gradle 檔案。

build.gradle

...
dependencies {
...
"androidx.compose.material3:material3-window-size-class:$material3_version"
...
  1. 新增依附元件後,按一下「Sync Now」即可同步處理 Gradle。

分頁標籤下方會顯示「Sync Now」按鈕,以供選取不同的 .kt 和 .gradle 檔案。「Sync Now」按鈕右側是另一個「Ignore」按鈕,選取此按鈕可忽略這些變更。

保持更新 build.grade 檔案,您就可以建立變數,隨時儲存應用程式視窗的大小。

  1. MainActivity.kt 檔案的 onCreate() 函式中,利用傳遞至參數的 this 結構定義指派 calculateWindowSizeClass 至名為 windowSize 的變數。
  2. 匯入適當的 calculateWindowSizeClass 套件。

MainActivity.kt

...
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass

...

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

        setContent {
            ReplyTheme {
                val windowSize = calculateWindowSizeClass(this)
                ReplyApp()

...
  1. 此時您會發現請注意,calculateWindowSizeClass 語法出現紅色底,並顯示紅色的燈泡圖示。按一下 windowSize 變數左側的紅色燈泡,然後選取「Opt in for ‘ExperimentalMaterial3WindowSizeClassApi' on ‘onCreate'」,以在 onCreate() 方法上方建立註解。

在程式碼中,選取「val windowSize = computeWindowSizeClass(this)」行,右側的燈泡圖示會顯示在程式碼的左側。所選燈泡下方會列出錯誤的解決方式選項,並選取「Opt in for 'ExperimentalMaterial3WindowSizeClassApi' on 'onCreate'」。

您可以使用 MainActivity.kt 中的 WindowWidthSizeClass 變數,決定要在不同可組合項中顯示的版面配置。現在讓我們來準備 ReplyApp 可組合項以接收此值。

  1. ReplyApp.kt 檔案中,修改 ReplyApp 可組合項以接受 WindowWidthSizeClass 做為參數,並匯入適當的套件。

ReplyApp.kt

...
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
...

@Composable
fun ReplyApp(
    windowSize: WindowWidthSizeClass,
    modifier: Modifier = Modifier
) {
...
  1. windowSize 變數傳遞至 MainActivity.kt 檔案 onCreate() 方法中的 ReplyApp 元件。

MainActivity.kt

...
         setContent {
            ReplyTheme {
                val windowSize = calculateWindowSizeClass(this)
                ReplyApp(
                    windowSize = windowSize.widthSizeClass
                )
...

此外,您也需要更新 windowSize 參數的應用程式預覽。

  1. WindowWidthSizeClass.Compact 做為 windowSize 參數傳遞至預覽元件的 ReplyApp 可組合項,然後匯入適當的套件。

MainActivity.kt

...
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
...

@Preview(showBackground = true)
@Composable
fun ReplyAppPreview() {
    ReplyTheme {
        ReplyApp(
            windowSize = WindowWidthSizeClass.Compact,
        )
    }
}
  1. 如要根據螢幕大小變更應用程式版面配置,請根據 WindowWidthSizeClass 值在 ReplyApp 可組合項中新增 when 陳述式。

ReplyApp.kt

...

@Composable
fun ReplyApp(
    windowSize: WindowWidthSizeClass,
    modifier: Modifier = Modifier
) {
    val viewModel: ReplyViewModel = viewModel()
    val replyUiState = viewModel.uiState.collectAsState().value

    when (windowSize) {
        WindowWidthSizeClass.Compact -> {
        }
        WindowWidthSizeClass.Medium -> {
        }
        WindowWidthSizeClass.Expanded -> {
        }
        else -> {
        }
    }
...

此時您已經打好基礎,可使用 WindowSizeClass 值變更應用程式的版面配置。下一步是要決定您的應用程式在不同大小螢幕中的呈現效果。

7. 實作自動調整導覽版面配置

實作自動調整 UI 導覽

目前所有螢幕大小的配置都使用底部導覽

Reply 應用程式的底部導覽。

如先前所述,此導覽元素並不理想,因為使用者如果使用較大的螢幕,要觸及這些重要的導覽元素可能會有困難。不用擔心,回應式 UI 導覽功能提供了適合不同視窗大小類別使用的建議模式。在 Reply 應用程式中,您可以實作下列元素:

表格中列出視窗大小類別和要顯示的幾個項目。精簡寬度會顯示底部導覽列。中等寬度會顯示導覽邊欄。展開寬度會顯示含有前端的導覽匣。

導覽邊欄質感設計的另一個導覽元件,可讓使用者從應用程式側邊存取主要目的地的精簡導覽選項。

Reply 應用程式中的導覽邊欄範例以垂直方式顯示 4 個圖示:收件匣、寄件備份、草稿和垃圾郵件。

同樣地,固定式/持續顯示的導覽匣也是由質感設計建立的另一個選項,可為較大的螢幕畫面提供符合人體工學的存取方式。

Reply 應用程式中的固定式導覽匣會垂直列出 4 個有分頁圖示和名稱的分頁標籤:收件匣、寄件備份、草稿和垃圾郵件。

實作導覽匣

如要建立適合展開畫面使用的導覽匣,您可以使用 navigationType 參數。請執行下列步驟以完成設定:

  1. 如要表示不同類型的導覽元素,請在 ui 目錄的新套件 utils 中建立新檔案 WindowStateUtils.kt
  2. 新增 Enum 類別以代表不同類型的導覽元素。

WindowStateUtils.kt

package com.example.reply.ui.utils

enum class ReplyNavigationType {
    BOTTOM_NAVIGATION, NAVIGATION_RAIL, PERMANENT_NAVIGATION_DRAWER
}

如要完成實作導覽匣,您必須根據應用程式的視窗大小決定導覽類型。

  1. ReplyApp 可組合項中,建立 navigationType 變數,並根據 when 陳述式中的螢幕大小指派適當的 ReplyNavigationType 值。

ReplyApp.kt

...
import com.example.reply.ui.utils.ReplyNavigationType
...
    when (windowSize) {
        WindowWidthSizeClass.Compact -> {
            navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
        }
        WindowWidthSizeClass.Medium -> {
            navigationType = ReplyNavigationType.NAVIGATION_RAIL
        }
        WindowWidthSizeClass.Expanded -> {
            navigationType = ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
        }
        else -> {
            navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
        }
    }
...

您可以在 ReplyHomeScreen 可組合項中使用 navigationType 值。如要執行此操作,請先將其設為可組合項的參數。

  1. ReplyHomeScreen 可組合項中,新增 navigationType 做為參數。

ReplyHomeScreen.kt

...
@Composable
fun ReplyHomeScreen(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit = {},
    onEmailCardPressed: (Email) -> Unit = {},
    onDetailScreenBackPressed: () -> Unit = {},
    modifier: Modifier = Modifier
)

...

  1. navigationType 傳遞至 ReplyHomeScreen 可組合項。

ReplyApp.kt

...
   ReplyHomeScreen(
        navigationType = navigationType,
        replyUiState = replyUiState,
        onTabPressed = { mailboxType: MailboxType ->
            viewModel.updateCurrentMailbox(mailboxType = mailboxType)
            viewModel.resetHomeScreenStates()
        },
        onEmailCardPressed = { email: Email ->
            viewModel.updateDetailsScreenStates(
                email = email
            )
        },
        onDetailScreenBackPressed = {
            viewModel.resetHomeScreenStates()
        },
        modifier = modifier
    )
...

接下來,您可以建立分支版本,以便在使用者於展開螢幕中開啟應用程式並顯示主畫面時,顯示有導覽匣的應用程式內容。

  1. ReplyHomeScreen 可組合項主體中,新增 navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER && replyUiState.isShowingHomepage 條件的 if 陳述式。

ReplyHomeScreen.kt

import androidx.compose.material3.PermanentNavigationDrawer
...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReplyHomeScreen(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit = {},
    onEmailCardPressed: (Email) -> Unit = {},
    onDetailScreenBackPressed: () -> Unit = {},
    modifier: Modifier = Modifier
) {
...
    if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
        && replyUiState.isShowingHomepage
    ) {
    }

    if (replyUiState.isShowingHomepage) {
            ReplyAppContent(
                replyUiState = replyUiState,
...
  1. 如要建立固定式導覽匣,請在 if 陳述式的主體中建立 PermanentNavigationDrawer 可組合項,然後新增 NavigationDrawerContent 可組合項作為 drawerContent 參數的輸入內容。
  2. ReplyAppContent 可組合項新增為 PermanentNavigationDrawer.的最終 lambda引數。

ReplyHomeScreen.kt

...
    if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
        && replyUiState.isShowingHomepage
    ) {
        PermanentNavigationDrawer(
            drawerContent = {
                NavigationDrawerContent(
                    selectedDestination = replyUiState.currentMailbox,
                    onTabPressed = onTabPressed,
                    navigationItemContentList = navigationItemContentList
                )
            }
        ) {
            ReplyAppContent(
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier

            )
        }
    }

...
  1. 新增 else 分支版本,以便使用先前的可組合主體針對非展開式螢幕保留之前的分支版本設定。

ReplyHomeScreen.kt

...
    if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
        && replyUiState.isShowingHomepage
    ) {
        PermanentNavigationDrawer(
            drawerContent = {
                NavigationDrawerContent(
                    selectedDestination = replyUiState.currentMailbox,
                    onTabPressed = onTabPressed,
                    navigationItemContentList = navigationItemContentList
                )
            }
        ) {
            ReplyAppContent(
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier

            )
        }
    } else {
        if (replyUiState.isShowingHomepage) {
            ReplyAppContent(
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
        } else {
            ReplyDetailsScreen(
                replyUiState = replyUiState,
                onBackPressed = onDetailScreenBackPressed,
                modifier = modifier
            )
        }
    }
}
...
  1. ReplyHomeScreen 可組合項中新增實驗功能註解。由於 PermanentNavigationDrawer API 仍處於實驗階段,因此必須加入註解。

ReplyHomeScreen.kt

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReplyHomeScreen(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit = {},
    onEmailCardPressed: (Email) -> Unit = {},
    onDetailScreenBackPressed: () -> Unit = {},
    modifier: Modifier = Modifier
) {
...
  1. 在「平板電腦」模式中執行應用程式。您應該會看到以下畫面:

在平板電腦模式中,Reply 應用程式會在畫面左側顯示導覽匣,並在右側顯示電子郵件清單。

實作導覽邊欄

與導覽匣實作類似,您必須使用 navigationType 參數在導覽元素之間切換。

首先,我們要為中型螢幕加入導覽邊欄。

  1. 先新增 navigationType 做為參數,以便做好 ReplyAppContent 可組合項的準備。

ReplyHomeScreen.kt

...
@Composable
private fun ReplyAppContent(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: ((MailboxType) -> Unit) = {},
    onEmailCardPressed: (Email) -> Unit = {},
    navigationItemContentList: List<NavigationItemContent>,
    modifier: Modifier = Modifier
) {
...
  1. navigationType 值傳遞至兩個 ReplyAppContent 可組合項。

ReplyHomeScreen.kt

...
            ReplyAppContent(
                navigationType = navigationType,
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
        }
    } else {
        if (replyUiState.isShowingHomepage) {
            ReplyAppContent(
                navigationType = navigationType,
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
...

接下來,我們要新增分支版本,允許應用程式在某些情況下顯示導覽邊欄。

  1. ReplyAppContent 可組合項主體第一行中,於 AnimatedVisibility 可組合項周圍納入 ReplyNavigationRail 可組合項,然後將 visibility 參數設為 true (如果 ReplyNavigationType 值為 NavigationRail)。

ReplyHomeScreen.kt

...
@Composable
private fun ReplyAppContent(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: ((MailboxType) -> Unit) = {},
    onEmailCardPressed: (Email) -> Unit = {},
    navigationItemContentList: List<NavigationItemContent>,
    modifier: Modifier = Modifier
) {
        AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
            ReplyNavigationRail(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
            )
        }
        Column(
            modifier = Modifier
                .fillMaxSize()            .background(MaterialTheme.colorScheme.inverseOnSurface)
        ) {
            ReplyListOnlyContent(
                replyUiState = replyUiState,
                onEmailCardPressed = onEmailCardPressed,
                modifier = Modifier.weight(1f)
            )
            ReplyBottomNavigationBar(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
                navigationItemContentList = navigationItemContentList

            )
        }

}
...
  1. 如要正確對齊可組合項,請在 Row 可組合項中同時納入在 ReplyAppContent 主題中找到的 AnimatedVisibility 可組合項和 Column 可組合項。

ReplyHomeScreen.kt

...
@Composable
private fun ReplyAppContent(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: ((MailboxType) -> Unit) = {},
    onEmailCardPressed: (Email) -> Unit = {},
    navigationItemContentList: List<NavigationItemContent>,
    modifier: Modifier = Modifier
) {
    Row(modifier = modifier.fillMaxSize()) {
        AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
            ReplyNavigationRail(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
                navigationItemContentList = navigationItemContentList
            )
        }
        Column(
            modifier = Modifier
                .fillMaxSize()            .background(MaterialTheme.colorScheme.inverseOnSurface)
        ) {
            ReplyListOnlyContent(
                replyUiState = replyUiState,
                onEmailCardPressed = onEmailCardPressed,
                modifier = Modifier.weight(1f)
            )
            ReplyBottomNavigationBar(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
                navigationItemContentList = navigationItemContentList

            )
        }
    }
}
...

最後,我們要確保在某些情況下畫面會顯示底部導覽列。

  1. ReplyListOnlyContent 可組合項之後,使用 AnimatedVisibility 可組合項納入 ReplyBottomNavigationBar 可組合項。
  2. ReplyNavigationType 值為 BOTTOM_NAVIGATION 時,設定 visible 參數。

ReplyHomeScreen.kt

...
            ReplyListOnlyContent(
                replyUiState = replyUiState,
                onEmailCardPressed = onEmailCardPressed,
                modifier = Modifier.weight(1f)
            )
            AnimatedVisibility(visible = navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) {
                ReplyBottomNavigationBar(
                    currentTab = replyUiState.currentMailbox,
                    onTabPressed = onTabPressed,
                    navigationItemContentList = navigationItemContentList
                )
            }
...
  1. 在「未折疊的折疊式裝置」模式中執行應用程式。您應該會看到以下畫面:

Reply 應用程式在折疊式裝置中顯示,且導覽邊欄在畫面的左側顯示,電子郵件清單則在右側顯示。

8. 取得解決方案程式碼

完成程式碼研究室後,如要下載當中用到的程式碼,您可以使用以下 Git 指令:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-reply-app.git

cd basic-android-kotlin-compose-training-reply-app
git checkout nav-update

另外,您也可以下載存放區為 ZIP 檔案,然後解壓縮並在 Android Studio 中開啟。

如要查看解決方案程式碼,請前往 GitHub 檢視

9. 結語

恭喜!您完成了實作自動調整導覽版面配置,現在朝將 Reply 應用程式設為可依照所有螢幕大小自動調整的目標又邁進了一大步。您運用多種 Android 板型規格提升了使用者體驗。在接下來的程式碼研究室中,您將透過實作自動調整內容版面配置、測試和預覽功能,進一步提升自動調整應用程式的運用技巧。

記得使用 #AndroidBasics,透過社群媒體分享您的作品!

瞭解詳情