1. 簡介
在 Android 平台中開發應用程式的主要優點之一,就是能廣泛觸及各種板型規格的使用者,例如穿戴式裝置、摺疊式裝置、平板電腦、桌上型電腦,甚至電視等。使用應用程式時,使用者可能會希望在大螢幕裝置上使用相同的應用程式,以便充分運用擴大的空間。有越來越多的 Android 使用者會在不同螢幕尺寸的多種裝置上使用應用程式,並期望在所有裝置上都能享有高品質的使用者體驗。
到目前為止,您已經瞭解如何打造適合行動裝置的應用程式。在本程式碼研究室中,您將學習如何轉換應用程式,以便配合其他螢幕大小自動調整版面配置。您將使用可自動調整的導覽版面配置模式,這種模式不但美觀,而且在行動裝置和大螢幕裝置 (例如摺疊式裝置、平板電腦和桌上型電腦) 上都可使用。
必要條件
- 熟悉 Kotlin 程式設計的概念,包括類別、函式和條件式
- 熟悉
ViewModel
類別的使用方式 - 熟悉
Composable
函式的建立方式 - 有使用 Jetpack Compose 建構版面配置的經驗
- 瞭解如何使用裝置或模擬器執行應用程式
課程內容
- 如何為簡易應用程式建立不同畫面間的導覽功能,不用導覽圖
- 如何使用 Jetpack Compose 建立可自動調整的導覽版面配置
- 如何建立自訂返回處理常式
建構項目
- 您將在現有的 Reply 應用程式中實作動態導覽,讓版面配置能配合所有螢幕大小自動調整
成品會如下所示:
軟硬體需求
- 可連上網路、具備網路瀏覽器且已安裝 Android Studio 的電腦
- GitHub 存取權
2. 應用程式總覽
Reply 應用程式簡介
Reply 應用程式是功能類似電子郵件用戶端的多畫面應用程式。
應用程式包含 4 個不同類別,並分別透過以下分頁顯示:收件匣、寄件備份、草稿和垃圾郵件。
下載範例程式碼
在 Android Studio 中開啟 basic-android-kotlin-compose-training-reply-app
資料夾。
3. 範例程式碼逐步操作說明
Reply 應用程式的重要目錄
Reply 應用程式專案的資料和 UI 層分別位於不同目錄。ReplyViewModel
、ReplyUiState
和其他可組合函式位於 ui
目錄中;定義資料層和資料供應商類別的 data
和 enum
類別則位於 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
可組合函式做為主要可組合函式,並在其中宣告 viewModel
和 uiState
。各種 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 應用程式的步驟,即可在狀態異動時變更畫面:
- 在 Android Studio 中開啟範例程式碼。
- 在
ReplyHomeScreen.kt
的ReplyHomeScreen
可組合函式中,使用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 ) } }
您現在必須考慮使用者離開主畫面的情況,這時應該顯示詳細資料畫面。
- 新增
else
分支,並在主體加入ReplyDetailsScreen
可組合函式。新增replyUIState
、onDetailScreenBackPressed
和modifier
做為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
類別,即可支援不同畫面間的導覽功能。
建立自訂返回處理常式
使用 NavHost
可組合函式切換畫面的優點之一,就是先前多個畫面的路線會儲存在返回堆疊中。有了這些儲存的畫面,就能在使用者叫用系統返回鈕時,輕鬆導覽至上一個畫面。由於 Reply 應用程式並未使用 NavHost
,必須新增程式碼,透過人工處理返回按鈕。接下來,您就要進行這項工作。
請完成下列步驟,在 Reply 應用程式中建立自訂返回處理常式:
- 在
ReplyDetailsScreen
可組合函式的第一行,新增BackHandler
可組合函式。 - 在
BackHandler
可組合函式的主體中呼叫onBackPressed()
函式。
ReplyDetailsScreen.kt
... import androidx.activity.compose.BackHandler ... @Composable fun ReplyDetailsScreen( replyUiState: ReplyUiState, onBackPressed: () -> Unit, modifier: Modifier = Modifier ) { BackHandler { onBackPressed() } ...
5. 在大螢幕裝置上執行應用程式
使用可調整大小的模擬器查看應用程式
為了製作實用的應用程式,開發人員需要瞭解各種板型規格的使用者體驗。因此,您必須從開發程序初期,就在各種板型規格上測試應用程式。
您可以使用多種不同螢幕大小的模擬器來達成這個目標。但是這樣做可能會相當繁瑣,尤其是要同時針對多種螢幕大小建構應用程式時。您可能也需要測試執行中的應用程式如何根據螢幕大小變更做出回應,例如螢幕方向變更、電腦上的視窗大小變更,以及摺疊式裝置的摺疊狀態變更。
Android Studio 導入可調整大小的模擬器,有助於這類情境的測試。
如要設定可調整大小的模擬器,請完成下列步驟:
- 在 Android Studio 中,依序選取「Tools」>「Device Manager」。
- 在「Device Manager」中,按一下「+」 圖示來建立虛擬裝置。
- 依序選取「Phone」類別 >「Resizable (Experimental)」裝置。
- 點選「Next」。
- 選取「API Level 34」或更高級別。
- 點選「Next」。
- 為新的 Android 虛擬裝置命名。
- 按一下「Finish」。
在大螢幕模擬器上執行應用程式
現在您已設定可調整大小的模擬器,讓我們看看應用程式在大螢幕上呈現的效果。
- 在可調整大小的模擬器中執行應用程式。
- 選取「Tablet」做為顯示模式。
- 在橫向模式下,以平板模式檢查應用程式。
請注意,平板螢幕的畫面會朝水平方向延長。雖然此螢幕方向在功能上運作正常,但可能不是大螢幕空間的最佳使用方式。接下來讓我們解決這個問題。
大螢幕專用設計
在平板電腦上看到此應用程式時,您的第一個想法可能是,這個應用程式不僅設計不良,還缺乏吸引力。沒錯,這個版面配置「並非」專為大螢幕設計。
為平板電腦和摺疊式裝置等大螢幕設計時,您必須考量使用者人體工學,以及使用者的手指是否接近螢幕。使用行動裝置時,使用者的手指可以輕鬆觸及大部分畫面,因此按鈕和導覽元素等互動元素的位置不會有嚴重影響。但在大螢幕上,如果將重要互動元素放在畫面中央,可能會較難觸及。
就像在 Reply 應用程式中,大螢幕專用設計並非只是配合螢幕大小而延展或放大 UI 元素。您可以善用這些擴大的空間,為使用者打造不同的使用體驗。舉例來說,您可以在同一個畫面加入另一個版面配置,省去導覽至另一個畫面的麻煩,也能進行多工處理。
此設計不但可以提升使用者的工作效率,還能進一步提高參與度。但是在部署此設計之前,必須先瞭解如何為不同螢幕大小分別建立版面配置。
6. 配合不同螢幕大小調整版面配置
什麼是中斷點?
您可能想知道要如何為同一個應用程式顯示不同的版面配置。簡單來說,就是針對不同的狀態使用條件式,也就是您在本程式碼研究室開頭採用的做法。
如要建立自動調整式應用程式,您需要根據螢幕大小變更版面配置。變更版面配置的測量點稱為中斷點。Material Design 建立了固定的中斷點範圍,涵蓋了大多數的 Android 螢幕。
舉例來說,據此中斷點範圍表格所示,如果應用程式目前是在螢幕大小不到 600 dp 的裝置上執行,就應顯示行動裝置版面配置。
使用視窗大小類別
為 Compose 導入的 WindowSizeClass
API,可簡化 Material Design 中斷點的實作程序。
視窗大小類別引入三個大小類別:精簡、中等和展開 (同時針對寬度和高度)。
如要在 Reply 應用程式中實作 WindowSizeClass
API,請完成下列步驟:
- 在模組
build.gradle.kts
檔案中新增material3-window-size-class
依附元件。
build.gradle.kts
... dependencies { ... implementation("androidx.compose.material3:material3-window-size-class") ...
- 新增依附元件後,按一下「Sync Now」同步處理 Gradle。
更新 build.gradle.kts
檔案後,您就可以建立變數,隨時儲存應用程式視窗的大小。
- 在
MainActivity.kt
檔案的onCreate()
函式中,為windowSize
變數指派calculateWindowSizeClass()
方法,並在參數中傳入this
環境。 - 匯入適當的
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() ...
- 請注意,
calculateWindowSizeClass
語法出現紅色底線,並顯示紅色的燈泡圖示。按一下windowSize
變數左側的紅色燈泡圖示,並選取「Opt in for ‘ExperimentalMaterial3WindowSizeClassApi' on ‘onCreate'」,在onCreate()
方法上方建立註解。
您可以使用 MainActivity.kt
中的 WindowWidthSizeClass
變數,決定要在各種可組合函式中顯示哪個版面配置。讓我們準備 ReplyApp
可組合函式來接收這個值。
- 在
ReplyApp.kt
檔案中修改ReplyApp
可組合函式,以參數形式接受WindowWidthSizeClass
,並匯入適當的套件。
ReplyApp.kt
... import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass ... @Composable fun ReplyApp( windowSize: WindowWidthSizeClass, modifier: Modifier = Modifier ) { ...
- 在
MainActivity.kt
檔案的onCreate()
方法中,將windowSize
變數傳遞至ReplyApp
元件。
MainActivity.kt
... setContent { ReplyTheme { Surface { val windowSize = calculateWindowSizeClass(this) ReplyApp( windowSize = windowSize.widthSizeClass ) ...
此外,您也需要根據 windowSize
參數更新應用程式的預覽畫面。
- 將
WindowWidthSizeClass.Compact
做為windowSize
參數傳遞至預覽元件的ReplyApp
可組合函式,並匯入適當的套件。
MainActivity.kt
... import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass ... @Preview(showBackground = true) @Composable fun ReplyAppCompactPreview() { ReplyTheme { Surface { ReplyApp( windowSize = WindowWidthSizeClass.Compact, ) } } }
- 如要配合螢幕大小變更應用程式版面配置,請根據
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 導覽
目前,所有螢幕大小都使用底部導覽功能。
如前所述,這個導覽元素並不理想,因為在大型螢幕上,使用者可能難以觸及這些重要導覽元素。幸運的是,回應式 UI 導覽功能提供了適用於不同視窗大小類別的建議模式。您可以為 Reply 應用程式實作下列元素:
導覽邊欄是 Material Design 的另一個導覽元件,可讓使用者從應用程式側邊,存取前往主要目的地的精簡導覽選項。
同樣地,另一個選項是透過 Material Design 建立的永久性導覽匣,可為大螢幕提供符合人體工學的存取方式。
實作導覽匣
您可以使用 navigationType
參數建立適用於展開式畫面的導覽匣,方法是完成下列步驟:
- 如要表示不同類型的導覽元素,請在新套件
utils
的ui
目錄中建立新檔案WindowStateUtils.kt
。 - 新增
Enum
類別,表示不同類型的導覽元素。
WindowStateUtils.kt
package com.example.reply.ui.utils enum class ReplyNavigationType { BOTTOM_NAVIGATION, NAVIGATION_RAIL, PERMANENT_NAVIGATION_DRAWER }
如要成功實作導覽匣,請根據應用程式的視窗大小決定導覽類型。
- 在
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
值,只要將其設為此可組合函式的參數,即可完成準備工作。
- 在
ReplyHomeScreen
可組合函式中,新增navigationType
做為參數。
ReplyHomeScreen.kt
... @Composable fun ReplyHomeScreen( navigationType: ReplyNavigationType, replyUiState: ReplyUiState, onTabPressed: (MailboxType) -> Unit, onEmailCardPressed: (Email) -> Unit, onDetailScreenBackPressed: () -> Unit, modifier: Modifier = Modifier ) ...
- 將
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 ) ...
接下來,您可以建立分支,以便在使用者於展開式螢幕上開啟應用程式並顯示主畫面時,顯示含導覽匣的應用程式內容。
- 在
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, ...
- 如要建立永久性導覽匣,請在 if 陳述式的主體中建立
PermanentNavigationDrawer
可組合函式,然後新增NavigationDrawerContent
可組合函式做為drawerContent
參數的輸入內容。 - 將
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 ) } } ...
- 新增
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 ) } } } ...
- 在「Tablet」模式下執行應用程式。您應該會看到以下畫面:
實作導覽邊欄
與實作導覽匣時相似,您需要使用 navigationType
參數來切換導覽元素。
首先,我們要為中等螢幕加入導覽邊欄。
- 新增
navigationType
做為參數,先準備好ReplyAppContent
可組合函式。
ReplyHomeScreen.kt
... @Composable private fun ReplyAppContent( navigationType: ReplyNavigationType, replyUiState: ReplyUiState, onTabPressed: ((MailboxType) -> Unit), onEmailCardPressed: (Email) -> Unit, navigationItemContentList: List<NavigationItemContent>, modifier: Modifier = Modifier ) { ...
- 將
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 ) ...
接下來,我們要新增分支,讓應用程式在某些情境下顯示導覽邊欄。
- 在
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() ) } } } ...
- 如要正確對齊可組合函式,請將
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() ) } } } ...
最後,我們要確保在某些情境下,應用程式會顯示底部導覽列。
- 在
ReplyListOnlyContent
可組合函式之後,使用AnimatedVisibility
可組合函式包裝ReplyBottomNavigationBar
可組合函式。 - 當
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() ) } ...
- 在「Unfolded foldable」模式下執行應用程式。您應該會看到以下畫面:
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,透過社群媒體分享您的作品!