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

1. 簡介

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

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

必要條件

  • 熟悉 Kotlin 程式設計的概念,包括類別、函式和條件式
  • 熟悉 ViewModel 類別的使用方式
  • 熟悉 Composable 函式的建立方式
  • 有使用 Jetpack Compose 建構版面配置的經驗
  • 瞭解如何使用裝置或模擬器執行應用程式

課程內容

  • 如何為簡易應用程式建立不同畫面間的導覽功能,不用導覽圖
  • 如何使用 Jetpack Compose 建立可自動調整的導覽版面配置
  • 如何建立自訂返回處理常式

建構項目

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

成品會如下所示:

56cfa13ef31d0b59.png

​​

軟硬體需求

  • 可連上網路、具備網路瀏覽器且已安裝 Android Studio 的電腦
  • GitHub 存取權

2. 應用程式總覽

Reply 應用程式簡介

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

a1af0f9193718abf.png

應用程式包含 4 個不同類別,並分別透過以下分頁顯示:收件匣、寄件備份、草稿和垃圾郵件。

下載範例程式碼

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

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

Reply 應用程式的重要目錄

Reply 應用程式檔案目錄顯示兩個已展開的子目錄:

Reply 應用程式專案的資料和 UI 層分別位於不同目錄。ReplyViewModelReplyUiState 和其他可組合函式位於 ui 目錄中;定義資料層和資料供應商類別的 dataenum 類別則位於 data 目錄中。

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

Reply 應用程式會透過 ReplyViewModel 中的 initializeUIState() 方法完成資料初始化,這項程序是在 init 函式中執行。

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 可組合函式中,使用 if 陳述式包裝 ReplyAppContent 可組合函式,用於 replyUiState 物件的 isShowingHomepage 屬性為 true 的情況。

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. 新增 else 分支,並在主體加入 ReplyDetailsScreen 可組合函式。新增 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 類別,即可支援不同畫面間的導覽功能。

8443a3ef1a239f6e.gif

建立自訂返回處理常式

使用 NavHost 可組合函式切換畫面的優點之一,就是先前多個畫面的路線會儲存在返回堆疊中。有了這些儲存的畫面,就能在使用者叫用系統返回鈕時,輕鬆導覽至上一個畫面。由於 Reply 應用程式並未使用 NavHost,必須新增程式碼,透過人工處理返回按鈕。接下來,您就要進行這項工作。

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

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

ReplyDetailsScreen.kt

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

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

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

為了製作實用的應用程式,開發人員需要瞭解各種板型規格的使用者體驗。因此,您必須從開發程序初期,就在各種板型規格上測試應用程式。

您可以使用多種不同螢幕大小的模擬器來達成這個目標。但是這樣做可能會相當繁瑣,尤其是要同時針對多種螢幕大小建構應用程式時。您可能也需要測試執行中的應用程式如何根據螢幕大小變更做出回應,例如螢幕方向變更、電腦上的視窗大小變更,以及摺疊式裝置的摺疊狀態變更。

Android Studio 導入可調整大小的模擬器,有助於這類情境的測試。

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

  1. 在 Android Studio 中,依序選取「Tools」>「Device Manager」

「Tools」選單顯示選項清單,已選取清單中的「Device Manager」。

  1. 在「Device Manager」中,按一下「+」 圖示來建立虛擬裝置。

裝置管理工具的工具列顯示兩個選單選項,包括「Create Virtual Device」。

  1. 依序選取「Phone」類別 >「Resizable (Experimental)」裝置。
  2. 點選「Next」

「Device Manager」視窗顯示選擇裝置定義的提示。顯示的選項清單上方有搜尋欄位。類別

  1. 選取「API Level 34」或更高級別。
  2. 點選「Next」

「Virtual Device Configuration」視窗顯示選取系統映像檔的提示。已選取「34」API 級別。

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

畫面顯示 Android 虛擬裝置 (AVD) 中的「Virtual Device Configuration」。設定畫面提供用來輸入 AVD 名稱的文字欄位。名稱欄位下方列出裝置選項,包括裝置定義 (Resizable Experimental)、系統映像檔 (Tiramisu) 和螢幕方向 (預設選取「Portrait」)。按鈕標示著

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

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

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

bfacf9c20a30b06b.png

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

bb0fa5e954f6ca4b.png

請注意,平板螢幕的畫面會朝水平方向延長。雖然此螢幕方向在功能上運作正常,但可能不是大螢幕空間的最佳使用方式。接下來讓我們解決這個問題。

大螢幕專用設計

在平板電腦上看到此應用程式時,您的第一個想法可能是,這個應用程式不僅設計不良,還缺乏吸引力。沒錯,這個版面配置「並非」專為大螢幕設計

為平板電腦和摺疊式裝置等大螢幕設計時,您必須考量使用者人體工學,以及使用者的手指是否接近螢幕。使用行動裝置時,使用者的手指可以輕鬆觸及大部分畫面,因此按鈕和導覽元素等互動元素的位置不會有嚴重影響。但在大螢幕上,如果將重要互動元素放在畫面中央,可能會較難觸及。

就像在 Reply 應用程式中,大螢幕專用設計並非只是配合螢幕大小而延展或放大 UI 元素。您可以善用這些擴大的空間,為使用者打造不同的使用體驗。舉例來說,您可以在同一個畫面加入另一個版面配置,省去導覽至另一個畫面的麻煩,也能進行多工處理。

f50e77a4ffd923a.png

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

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

什麼是中斷點?

您可能想知道要如何為同一個應用程式顯示不同的版面配置。簡單來說,就是針對不同的狀態使用條件式,也就是您在本程式碼研究室開頭採用的做法。

如要建立自動調整式應用程式,您需要根據螢幕大小變更版面配置。變更版面配置的測量點稱為中斷點。Material Design 建立了固定的中斷點範圍,涵蓋了大多數的 Android 螢幕。

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

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

使用視窗大小類別

為 Compose 導入的 WindowSizeClass API,可簡化 Material Design 中斷點的實作程序。

視窗大小類別引入三個大小類別:精簡、中等和展開 (同時針對寬度和高度)。

此圖表為寬度視窗大小類別的示意圖。 此圖表為高度視窗大小類別的示意圖。

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

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

build.gradle.kts

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

b4c912a45fa8b7f4.png

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

  1. MainActivity.kt 檔案的 onCreate() 函式中,為 windowSize 變數指派 calculateWindowSizeClass() 方法,並在參數中傳入 this 環境。
  2. 匯入適當的 calculateWindowSizeClass 套件。

MainActivity.kt

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

...

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

    setContent {
        ReplyTheme {
            val layoutDirection = LocalLayoutDirection.current
            Surface (
               // ...
            ) {
                val windowSize = calculateWindowSizeClass(this)
                ReplyApp()
...  
  1. 請注意,calculateWindowSizeClass 語法出現紅色底線,並顯示紅色的燈泡圖示。按一下 windowSize 變數左側的紅色燈泡圖示,並選取「Opt in for ‘ExperimentalMaterial3WindowSizeClassApi' on ‘onCreate'」,在 onCreate() 方法上方建立註解。

f8029f61dfad0306.png

您可以使用 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. MainActivity.kt 檔案的 onCreate() 方法中,將 windowSize 變數傳遞至 ReplyApp 元件。

MainActivity.kt

...
        setContent {
            ReplyTheme {
                Surface {
                    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 ReplyAppCompactPreview() {
    ReplyTheme {
        Surface {
            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 導覽

目前,所有螢幕大小都使用底部導覽功能。

f39984211e4dd665.png

如前所述,這個導覽元素並不理想,因為在大型螢幕上,使用者可能難以觸及這些重要導覽元素。幸運的是,回應式 UI 導覽功能提供了適用於不同視窗大小類別的建議模式。您可以為 Reply 應用程式實作下列元素:

表格列出視窗大小類別和顯示的幾個項目。「Compact」寬度會顯示底部導覽列。「Medium」寬度會顯示導覽邊欄。「Expanded」寬度會顯示含有前端的永久性導覽匣。

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

1c73d20ace67811c.png

同樣地,另一個選項是透過 Material Design 建立的永久性導覽匣,可為大螢幕提供符合人體工學的存取方式。

6795fb31e6d4a564.png

實作導覽匣

您可以使用 navigationType 參數建立適用於展開式畫面的導覽匣,方法是完成下列步驟:

  1. 如要表示不同類型的導覽元素,請在新套件 utilsui 目錄中建立新檔案 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
...
    val navigationType: 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
...
@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 = {
                PermanentDrawerSheet(Modifier.width(dimensionResource(R.dimen.drawer_width))) {
                    NavigationDrawerContent(
                        selectedDestination = replyUiState.currentMailbox,
                        onTabPressed = onTabPressed,
                        navigationItemContentList = navigationItemContentList,
                        modifier = Modifier
                            .wrapContentWidth()
                            .fillMaxHeight()
                            .background(MaterialTheme.colorScheme.inverseOnSurface)
                            .padding(dimensionResource(R.dimen.drawer_padding_content))
                    )
                }
            }
        ) {
            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 = {
                PermanentDrawerSheet(Modifier.width(dimensionResource(R.dimen.drawer_width))) {
                    NavigationDrawerContent(
                        selectedDestination = replyUiState.currentMailbox,
                        onTabPressed = onTabPressed,
                        navigationItemContentList = navigationItemContentList,
                        modifier = Modifier
                            .wrapContentWidth()
                            .fillMaxHeight()
                            .background(MaterialTheme.colorScheme.inverseOnSurface)
                            .padding(dimensionResource(R.dimen.drawer_padding_content))
                    )
                }
            }
        ) {
            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. 在「Tablet」模式下執行應用程式。您應該會看到以下畫面:

2dbbc2f88d08f6a.png

實作導覽邊欄

與實作導覽匣時相似,您需要使用 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 可組合函式主體第一行中,將 ReplyNavigationRail 可組合函式包裝在 AnimatedVisibility 可組合函式中,然後將 visible 參數設為 true (如果 ReplyNavigationType 值為 NAVIGATION_RAIL)。

ReplyHomeScreen.kt

...
@Composable
private fun ReplyAppContent(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: ((MailboxType) -> Unit),
    onEmailCardPressed: (Email) -> Unit,
    navigationItemContentList: List<NavigationItemContent>,
    modifier: Modifier = Modifier
) {
    Box(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)
                    .padding(
                        horizontal = dimensionResource(R.dimen.email_list_only_horizontal_padding)
                    )
            )
            ReplyBottomNavigationBar(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
                navigationItemContentList = navigationItemContentList,
                  modifier = Modifier
                      .fillMaxWidth()
            )
        }
    }
}     
... 
  1. 如要正確對齊可組合函式,請將 ReplyAppContent 主體中的 AnimatedVisibility 可組合函式和 Column 可組合函式,包裝在 Row 可組合函式中。

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) {
        AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
            val navigationRailContentDescription = stringResource(R.string.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)
                    .padding(
                        horizontal = dimensionResource(R.dimen.email_list_only_horizontal_padding)
                )
            )
            ReplyBottomNavigationBar(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = Modifier
                    .fillMaxWidth()
            )
        }
    }
}

... 

最後,我們要確保在某些情境下,應用程式會顯示底部導覽列。

  1. ReplyListOnlyContent 可組合函式之後,使用 AnimatedVisibility 可組合函式包裝 ReplyBottomNavigationBar 可組合函式。
  2. ReplyNavigationType 值為 BOTTOM_NAVIGATION 時,設定 visible 參數。

ReplyHomeScreen.kt

...
ReplyListOnlyContent(
    replyUiState = replyUiState,
    onEmailCardPressed = onEmailCardPressed,
    modifier = Modifier.weight(1f)
        .padding(
            horizontal = dimensionResource(R.dimen.email_list_only_horizontal_padding)
        )

)
AnimatedVisibility(visible = navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) {
    val bottomNavigationContentDescription = stringResource(R.string.navigation_bottom)
    ReplyBottomNavigationBar(
        currentTab = replyUiState.currentMailbox,
        onTabPressed = onTabPressed,
        navigationItemContentList = navigationItemContentList,
        modifier = Modifier
            .fillMaxWidth()
    )
}

... 
  1. 在「Unfolded foldable」模式下執行應用程式。您應該會看到以下畫面:

bfacf9c20a30b06b.png

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,透過社群媒體分享您的作品!

瞭解詳情