Jetpack Compose Navigation

1. 簡介

上次更新時間: 2021 年 3 月 17 日

軟硬體需求

導覽是 Jetpack 的程式庫,能讓您沿著特定路徑,從應用程式中的一個目的地導航至另一個目的地。導覽程式庫也提供了特定成果,方便您在透過 Jetpack Compose 構建的畫面中,完成一致且慣用的導覽。這個成果 (navigation-compose) 是本程式碼研究室的關注重點。

要執行的步驟

您將使用 Rally Material study 作為此程式碼研究室的基礎。您將遷移現有的導覽程式碼以使用 Jetpack Navigation 元件在 Jetpack Compose 切換畫面中進行導覽。

課程內容

  • 使用 Jetpack Navigation 搭配 Jetpack Compose 的基本概念
  • 在可組合項之間進行導覽
  • 使用必要和選用引數進行導覽
  • 使用深層連結進行導覽
  • 將 TabBar 整合至導覽階層
  • 測試導覽

2. 設定

你可以按照本程式碼研究室在電腦上的指示操作。

如要自行操作,請複製程式碼研究室的起點。

$ git clone https://github.com/googlecodelabs/android-compose-codelabs.git

或者,您也可以下載兩個 ZIP 檔案:

現在您已下載程式碼,請開啟 Android Studio 中的 NavigationCodelab 專案。您可以開始使用了。

3. 遷至導航

Rally 是一開始未使用 Navigation 的現有應用程式。遷移步驟如下:

  1. 新增 Navigation 依附元件
  2. 設定 NavController 和 NavHost
  3. 準備目的地的路徑
  4. 以導覽路徑取代原本的目的地機制

新增 Navigation 依附元件

開啟在 app/build.gradle 中的應用程式建構檔案。在依附元件區段中,新增 navigation-compose 依附元件。

dependencies {
  implementation "androidx.navigation:navigation-compose:2.4.0-beta02"
  // other dependencies
}

接著,請同步處理專案,您隨即便可以開始在 Compose 中使用「導覽」了。

設定 NavController

在 Compose 中使用「導覽」時,NavController 是中心元件,能夠追蹤返回堆疊項目、往前移動堆疊、啟用返回堆疊操控,以及在畫面狀態間進行導覽。因為 NavController 是導航核心,因此必須首先建立以導覽至目的地。

在 Compose 中,您使用的會是 NavController 子類別的 NavHostController。使用 rememberNavController() 函式取得 NavController;這會建立並記住 NavController,其在設定變更後仍然有效 (使用 rememberSavable)。NavController 已與一個 NavHost 可組合項建立關聯。NavHost 會將 NavController 與指定可組合目的地的導覽圖表建立連結。

對於本程式碼研究室,請取得 RallyApp 並將其請儲存於 NavController 中。這是整個應用程式的根元件,可在 RallyActivity.kt 中找到。

import androidx.navigation.compose.rememberNavController
...

@Composable
fun RallyApp() {
    RallyTheme {
        val allScreens = RallyScreen.values().toList()
        var currentScreen by rememberSaveable { mutableStateOf(RallyScreen.Overview) }
        val navController = rememberNavController()
        Scaffold(...
}

準備目的地的路徑

總覽

Rally 應用程式有三個畫面:

  1. 總覽:所有財務交易和快訊總覽
  2. 帳戶:深入分析現有帳戶
  3. 帳單:預定支出

總覽畫面的螢幕截圖,當中包含「快訊」、「帳戶」和「帳單」的相關資訊。 「帳戶」畫面的螢幕截圖,其中包含數個帳戶的資訊。 「帳單」畫面的螢幕截圖,其中包含數個送出帳單的相關資訊。

這三個畫面都是使用可組合項加以建構。請看一下 RallyScreen.kt。這個檔案中會宣告三個畫面。您稍後會將這些畫面對應至導航目的地,並以 Overview 做為開始目的地。您也會將可組合項移出 RallyScreen 並移入 NavHost。並暫時保留 RallyScreen 不動。

在 Compose 中使用 Navigation 時,路徑會以字串表示。您可以將這類字串想成是類似網址或深層連結的東西。在這個程式碼研究室中,我們會使用每個 RallyScreen 項目的 name 屬性做為路徑,例如 RallyScreen.Overview.name

準備事項

返回 RallyActivity.kt 中的 RallyApp 可組合項,並將內含畫面內容的 Box 替換為新建立的 NavHost。傳入我們在上一個步驟中建立的 navControllerNavHost 也需要 startDestination。將其設定為 RallyScreen.Overview.name。此外,請建立 Modifier 以將邊框間距傳入 NavHost

import androidx.compose.foundation.layout.Box
import androidx.compose.material.Scaffold
import androidx.navigation.compose.NavHost
...

Scaffold(...) { innerPadding ->
        NavHost(
            navController = navController,
            startDestination = RallyScreen.Overview.name,
            modifier = Modifier.padding(innerPadding)
        ) { ... }

現在可以定義導覽圖了。NavHost 可導航到的目的地已準備好接受目的地。系統會使用 NavGraphBuilder 來執行上述操作,這會提供給 NavHost (一個用於定義導覽圖的 lambda) 的最後一個參數。由於這項參數需要一個函式,因此您可以在 trailing lambda 中宣告目的地。Navigation Compose 成果提供 NavGraphBuilder.composable 擴充功能。您可以用這個功能定義導覽圖中的導航目的地。

import androidx.navigation.compose.NavHost
...

NavHost(
    navController = navController,
    startDestination = RallyScreen.Overview.name,
    modifier = Modifier.padding(innerPadding)

) {
    composable(RallyScreen.Overview.name) { ... }
}

目前,我們會暫時設定 Text,並使用畫面的名稱做為可組合項的內容。在下一個步驟中,我們會使用現有的可組合項。

import androidx.compose.material.Text
import androidx.navigation.compose.composable
...

NavHost(
    navController = navController,
    startDestination = RallyScreen.Overview.name
    modifier = Modifier.padding(innerPadding)
) {
    composable(RallyScreen.Overview.name) {
      Text(text = RallyScreen.Overview.name)
    }

    // TODO: Add the other two screens
}

現在,請移除從 Scaffold 呼叫的 currentScreen.content 並執行應用程式;您將會看到上方的起點目的地的名稱和分頁。

最終您應該會看到像這樣的 NavHost:

NavHost(
    navController = navController,
    startDestination = RallyScreen.Overview.name,
    modifier = Modifier.padding(innerPadding)
) {
    composable(RallyScreen.Overview.name) {
      Text(RallyScreen.Overview.name)
    }
    composable(RallyScreen.Accounts.name) {
        Text(RallyScreen.Accounts.name)
    }
    composable(RallyScreen.Bills.name) {
        Text(RallyScreen.Bills.name)
    }
}

NavHost 現在可以取代 Scaffold 中的 Box。將 Modifier 傳入 NavHost,以保持 innerPadding 的完整。

@Composable
fun RallyApp() {
    RallyTheme {
        val allScreens = RallyScreen.values().toList()
        // FIXME: This duplicate source of truth
        //  will be removed later.
        var currentScreen by rememberSaveable {
            mutableStateOf(RallyScreen.Overview)
        }
        val navController = rememberNavController()
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreens = allScreens,
                    onTabSelected = { screen -> currentScreen = screen },
                    currentScreen = currentScreen
                )
            }
        ) { innerPadding ->
            NavHost(
                navController = navController,
                startDestination = RallyScreen.Overview.name,
                modifier = Modifier.padding(innerPadding)) {
            }
        }
    }
}

到目前為止,頂端列尚未連接完成,因此點按分頁標籤並不會變更顯示的可組合項。在下一個步驟中,您將處理這個問題。

完全整合導覽列狀態變更

在這個步驟中,您必須連接 RallyTabRow,並刪除目前的手動的導覽程式碼。完成這個步驟後,導覽元件就可以完全接手轉送。

同樣在 RallyActivity 中,您會發現當點按分頁標籤時,RallyTabRow 可組合項會有一個回呼 (稱為 onTabSelected)。更新選取程式碼以使用 navController 導航至所選畫面。

這就是使用導覽透過 TabRow 進行導航時所必要的所有內容:

@Composable
fun RallyApp() {
    RallyTheme {
        val allScreens = RallyScreen.values().toList()
        // FIXME: This duplicate source of truth
        //  will be removed later.
        var currentScreen by rememberSaveable {
            mutableStateOf(RallyScreen.Overview)
        }
        val navController = rememberNavController()
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreens = allScreens,
                    onTabSelected = { screen ->
                        navController.navigate(screen.name)
                },
                    currentScreen = currentScreen,
                )
            }

這項異動實施後,currentScreen 將不再更新。也就是說,所選項目的展開和收合功能將無法正常運作。如要重新啟用這項行為,則還需要更新 currentScreen 屬性。幸好,「導覽」為您保留了返回堆疊,並向您提供目前的返回堆疊項目做為 State。透過這個 State,您可以對返回堆疊的變更做出反應。您甚至可以查詢目前的路徑返回堆疊路徑。

如要完成將 TabRow 畫面選項遷移至 Navigation,請更新 currentScreen 以如這類的導覽堆疊。

import androidx.navigation.compose.currentBackStackEntryAsState
...

@Composable
fun RallyApp() {
    RallyTheme {
        val allScreens = RallyScreen.values().toList()
        val navController = rememberNavController()
        val backstackEntry = navController.currentBackStackEntryAsState()
        val currentScreen = RallyScreen.fromRoute(
            backstackEntry.value?.destination?.route
        )
        ...
    }
}

這個時候,當您在執行應用程式時,您可以使用分頁標籤來切換螢幕,但系統只會顯示螢幕名稱。RallyScreen 必須先遷移至導覽,才能顯示螢幕。

將 RallyScreen 移至 Navigation

完成這個步驟之後,可組合項就會與 RallyScreen 列舉徹底中斷,並移至 NavHostRallyScreen 只會為提供螢幕圖示和標題而存在。

開啟 RallyScreen.kt。將每個螢幕的 body 導入移至 RallyAppNavHost 內的對應可組合項。

import com.example.compose.rally.data.UserData
import com.example.compose.rally.ui.accounts.AccountsBody
import com.example.compose.rally.ui.bills.BillsBody
import com.example.compose.rally.ui.overview.OverviewBody
...

NavHost(
    navController = navController,
    startDestination = Overview.name,
    modifier = Modifier.padding(innerPadding)
) {

    composable(Overview.name) {
        OverviewBody()
    }
    composable(Accounts.name) {
        AccountsBody(accounts = UserData.accounts)
    }
    composable(Bills.name) {
        BillsBody(bills = UserData.bills)
    }
}

此時,您可以從 RallyScreen 安全地移除 content 函式和 body 參數及其使用方法,此操作後的程式碼會如下所示:

enum class RallyScreen(
    val icon: ImageVector,
) {
    Overview(
        icon = Icons.Filled.PieChart,
    ),
    Accounts(
        icon = Icons.Filled.AttachMoney,
    ),
    Bills(
        icon = Icons.Filled.MoneyOff,
    );

    companion object {
        ...
    }
}

再次執行應用程式。您將看到原本的三個螢幕,並可透過 TabRow 在螢幕之間導航。

啟用 OverviewScreen 的點擊

在本程式碼研究室中,原本會忽略 OverviewBody 上的點擊事件。也就是說「SEE ALL」 (全部顯示) 按鈕雖然可點擊,但卻無法前往任何地方。

總覽螢幕的螢幕錄製、捲動至最終點擊目的地,然後嘗試點擊。點擊因為尚未導入而無法運作。

讓我們一起解決這個問題!

OverviewBody 可接受多種函式做為點擊事件的回呼。導入 onClickSeeAllAccountsonClickSeeAllBills 即可導航至相關的目的地。

如要在點按「SEE ALL」按鈕時啟用導覽功能,請使用 navController 並前往「Accounts」或「Bills」螢幕。開啟 RallyActivity.kt,在 NavHost 中尋找 OverviewBody,並新增導航呼叫。

OverviewBody(
    onClickSeeAllAccounts = { navController.navigate(Accounts.name) },
    onClickSeeAllBills = { navController.navigate(Bills.name) },
)

現在,您可以輕鬆為 OverviewBody 變更點擊事件的行為。只要將 navController 保持在導覽階層的頂層,而非將其直接傳遞到 OverviewBody,就能輕鬆進行單獨預覽或測試 OverviewBody,而不必在執行此操作時使用實際的 navController

4. 透過引數進行導覽

讓我們為 Rally 新增一些功能!將會新增一個「帳戶」螢幕,其中會顯示使用者點擊資料列時個別帳戶的詳細資料。

導航引數會動態調整路徑。導覽引數是一項強大的工具,能將一或多個引數傳遞至路徑並調整引數類型或預設值,讓轉送行為變得更加動態。

RallyActivity 中透過引數 Accounts/{name} 將新可組合項新增至現有的 NavHost 以將新目的地新增至導覽圖。我們也會為這個目的地指定一個 navArgument 清單。我們會定義名為「name」的 String 類型單一引數。

import androidx.navigation.NavType
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.navArgument
...

val accountsName = RallyScreen.Accounts.name

composable(
    route = "$accountsName/{name}",
    arguments = listOf(
        navArgument("name") {
            // Make argument type safe
            type = NavType.StringType
        }
    )
) {
    // TODO
}

每個 composable 目的地的主體會收到目前 NavBackStackEntry 的參數 (目前為止尚未使用過的),此程式碼會模擬當前目的地的路徑和引數。我們可以使用 arguments 擷取引數 (例如所選帳戶名稱),然後在 UserData 中查找此引數並將其傳送至 SingleAccountBody 可組合項。

如果未提供引數,則您也可以使用預設值。我們將略過這部分,因此引數在此處並非必要。

您的程式碼現在應如下所示:

import androidx.navigation.NavType
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.navArgument
...

val accountsName = RallyScreen.Accounts.name
NavHost(...) {
    ...
    composable(
        "$accountsName/{name}",
        arguments = listOf(
            navArgument("name") {
                // Make argument type safe
                type = NavType.StringType
            }
        )
    ) { entry -> // Look up "name" in NavBackStackEntry's arguments
        val accountName = entry.arguments?.getString("name")
        // Find first name match in UserData
        val account = UserData.getAccount(accountName)
        // Pass account to SingleAccountBody
        SingleAccountBody(account = account)
    }
}

現在已使用引數設定可組合項,因此可以使用 navController (例如:navController.navigate("${RallyScreen.Accounts.name}/$accountName")) 來導航至該引數。

將這項函式新增至 NavHostOverviewBody 宣告的 onAccountClick 參數以及 AccountsBodyonAccountClick 中。

如要讓該函式可重複使用,您可以建立私人輔助函式,如下所示。

fun RallyNavHost(
    ...
) {
    NavHost(
        ...
    ) {
        composable(Overview.name) {
            OverviewBody(
                ...
                onAccountClick = { name ->
                    navigateToSingleAccount(navController, name)
                },
            )
        }
        composable(Accounts.name) {
            AccountsBody(accounts = UserData.accounts) { name ->
                navigateToSingleAccount(
                    navController = navController,
                    accountName = name
                )
            }
        }
        ...
    }
}

private fun navigateToSingleAccount(
    navController: NavHostController,
    accountName: String
) {
    navController.navigate("${Accounts.name}/$accountName")
}

此時,當您執行應用程式時,可以點按每個帳戶,屆時系統會將您導向至顯示指定帳戶資料的螢幕。

總覽螢幕的螢幕錄製、捲動至最終點擊目的地,然後嘗試點擊。點擊現在會導向目的地。

5. 啟用深層連結支援

除了引數以外,您也可以使用深層連結,將應用程式中的目的地提供給第三方應用程式。在這個部分中,您將在前一個部分建立的路徑中新增深層連結,讓應用程式外的深層連結能夠直接根據名稱連結至個人帳戶。

新增意圖篩選器

首先,請將深層連結新增至 AndroidManifest.xml。您必須使用 VIEW 來建立 RallyActivity 的新意圖篩選器,並將 BROWSABLEDEFAULT 進行分類。

然後使用 data 標記新增 schemehostpathPrefix

這個程式碼研究室會使用 rally://accounts/{name} 做為深層連結網址。

您不需要在 AndroidManifest 中宣告「name」引數。Navigation 會將其做為引數加以剖析。

<activity
    android:name=".RallyActivity"
    android:windowSoftInputMode="adjustResize"
    android:label="@string/app_name"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="rally" android:host="accounts" />
    </intent-filter>
</activity>

您現在可以在 RallyActivity 中回應傳入的意圖。

您之前建立來接受引數的可組合項也可以接受新建立的深層連結。

使用 navDeepLink 函式新增 deepLinks 清單。傳遞 uriPattern 並提供上述 intent-filter 相符的 URI。使用 deepLinks 參數將建立的深層連結傳遞至可組合項。

val accountsName = RallyScreen.Accounts.name

composable(
    "$accountsName/{name}",
    arguments = listOf(
        navArgument("name") {
            type = NavType.StringType
        },
    ),
    deepLinks =  listOf(navDeepLink {
        uriPattern = "rally://$accountsName/{name}"
    })
)

現在您的應用程式已經能處理深層連結了。如要測試其能否正常運作,請在模擬器或裝置上安裝 Rally 的現有版本,開啟指令列並執行下列指令:

adb shell am start -d "rally://accounts/Checking" -a android.intent.action.VIEW

這樣就能直接進入查驗帳戶,並可處理應用程式中的所有帳戶名稱。

6. 擷取已完成的 NavHost

現在,您的 NavHost 已完成。您可以將其從 RallyApp 可組合項擷取到其專屬函式中,並將其命名為 RallyNavHost。這是唯一您應與 navController 直接一起使用的可組合項。如果未在 RallyNavHost 中建立 navController,您仍然可以用該可組合項選擇分頁標籤,這是 RallyApp 中較高層結構的一部分。

@Composable
fun RallyNavHost(
    navController: NavHostController,
    modifier: Modifier = Modifier
) {
    NavHost(
        navController = navController,
        startDestination = Overview.name,
        modifier = modifier
    ) {
        composable(Overview.name) {
            OverviewBody(
                onClickSeeAllAccounts = { navController.navigate(Accounts.name) },
                onClickSeeAllBills = { navController.navigate(Bills.name) },
                onAccountClick = { name ->
                    navController.navigate("${Accounts.name}/$name")
                },
            )
        }
        composable(Accounts.name) {
            AccountsBody(accounts = UserData.accounts) { name ->
                navController.navigate("Accounts/${name}")
            }
        }
        composable(Bills.name) {
            BillsBody(bills = UserData.bills)
        }
        val accountsName = Accounts.name
        composable(
            "$accountsName/{name}",
            arguments = listOf(
                navArgument("name") {
                    type = NavType.StringType
                },
            ),
            deepLinks = listOf(navDeepLink {
                uriPattern = "example://rally/$accountsName/{name}"
            }),
        ) { entry ->
            val accountName = entry.arguments?.getString("name")
            val account = UserData.getAccount(accountName)
            SingleAccountBody(account = account)
        }
    }
}

此外,請務必將原始呼叫網站替換為 RallyNavHost(navController),確保一切運作正常。

fun RallyApp() {
    RallyTheme {
    ...
        Scaffold(
        ...
        ) { innerPadding ->
            RallyNavHost(
                navController = navController,
                modifier = Modifier.padding(innerPadding)
            )

        }
     }
}

7. 在 Compose 中測試 Navigation

在此程式碼研究室的一開始,我們就確定不會將 navController 直接傳遞給任何的可組合項,而是傳遞回呼做為參數。也就是說,您的所有可組合項都可個別進行測試。不過,你也可以測試整個 NavHost,這就是這個步驟的所有內容。如要測試個別可組合函式,請務必查看 在 Jetpack Compose 中進行測試 的程式碼研究室。

準備測試類別

可以從 Activity 本身分隔出 NavHost 進行測試。

由於這項測試仍在 Android 裝置上執行,您必須在 /app/src/androidTest/java/com/example/compose/rallyandroidTest 目錄中建立測試檔案。

進行上述操作,並將其命名為 RallyNavHostTest

然後,若要使用 Compose 測試的 API,請創建如下的測試規則。

import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule

class RallyNavHostTest {

    @get:Rule
    val composeTestRule = createComposeRule()

}

現在您可以撰寫實際測試了。

撰寫您的第一則測試

建立測試函式,該函式必須設為公開,並使用 @Test 加註。在此函式中,您必須設定要測試的內容。請使用 composeTestRulesetContent 來操作。這裡使用 Composable 參數,讓您撰寫 Compose 程式碼,就像在一般應用程式中一樣。請依照您在 RallyActivity 中一樣的方式來設定 RallyNavHost

import androidx.navigation.compose.rememberNavController
import org.junit.Assert.fail
import org.junit.Test
...

class RallyNavHostTest {

    @get:Rule
    val composeTestRule = createComposeRule()
    lateinit var navController: NavHostController

    @Test
    fun rallyNavHost() {
        composeTestRule.setContent {
            navController = rememberNavController()
            RallyNavHost(navController = navController)
        }
        fail()
    }
}

如果您複製了上述程式碼,fail() 呼叫會確保您的測試一直失敗,直到進行實際宣告為止。這個呼叫會作為要您完成此測試的提醒。

您可以使用內容說明,確認顯示的螢幕是否正確。在本程式碼研究室中,我們會提供 "Accounts Screen""Overview Screen" 的內容說明,以便用於測試驗證。在測試類別中建立 lateinit 屬性,以便同樣用於日後的測試。

為了方便起見,請檢查 OverviewScreen 是否已顯示。

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.navigation.NavHostController
...

class RallyNavHostTest {

    @get:Rule
    val composeTestRule = createComposeRule()
    lateinit var navController: NavHostController

    @Test
    fun rallyNavHost() {
        composeTestRule.setContent {
            navController = rememberNavController()
            RallyNavHost(navController = navController)
        }
        composeTestRule
            .onNodeWithContentDescription("Overview Screen")
            .assertIsDisplayed()
    }
}

請移除 fail() 呼叫,然後再次執行測試並通過測試,就是這樣。

在下列各項測試中,RallyNavHost 的設定方式都相同。因此您可以將其擷取成加註 @Before 的函式,以確保程式碼能簡潔乾淨。

import org.junit.Before
...

class RallyNavHostTest {

    @get:Rule
    val composeTestRule = createComposeRule()
    lateinit var navController: NavHostController

    @Before
    fun setupRallyNavHost() {
        composeTestRule.setContent {
            navController = rememberNavController()
            RallyNavHost(navController = navController)
        }
    }

    @Test
    fun rallyNavHost() {
        composeTestRule
            .onNodeWithContentDescription("Overview Screen")
            .assertIsDisplayed()
    }
}

您可以透過多種方式來測試導入導覽,具體方法如下:點按導向新目的地的 UI 元素,或是以相應的路徑名稱呼叫 navigate

透過 UI 和測試規則進行測試

如要測試導入應用程式,建議您按一下使用者介面。請撰寫一則測試,以便點按「All Accounts」按鈕即可前往「Account Screen」,並驗證是否正確顯示螢幕。

import androidx.compose.ui.test.performClick
...

@Test
fun rallyNavHost_navigateToAllAccounts_viaUI() {
    composeTestRule
        .onNodeWithContentDescription("All Accounts")
        .performClick()
    composeTestRule
        .onNodeWithContentDescription("Accounts Screen")
        .assertIsDisplayed()
}

透過 UI 和 NavController 進行測試

你也可以使用 navController 來查看斷言。方法是在 UI 上執行點按操作,然後使用 backstackEntry.value?.destination?.route 比較目前路徑與預期路徑。

import androidx.compose.ui.test.performScrollTo
import org.junit.Assert.assertEquals
...

@Test
fun rallyNavHost_navigateToBills_viaUI() {
    // When click on "All Bills"
    composeTestRule.onNodeWithContentDescription("All Bills").apply {
        performScrollTo()
        performClick()
    }
    // Then the route is "Bills"
    val route = navController.currentBackStackEntry?.destination?.route
    assertEquals(route, "Bills")
}

透過 NavController 進行測試

第三種方式是直接呼叫 navController.navigate,在此處只有一個列表。必須在 UI 執行緒上呼叫 navController.navigate。如要這麼做,請將 Coroutines 搭配 Main 執行緒分派程式一起使用。由於呼叫必須在您對新狀態執行斷言 之前 進行,因此必須納入 runBlocking 呼叫。

runBlocking {
    withContext(Dispatchers.Main) {
        navController.navigate(RallyScreen.Accounts.name)
    }
}

之後您就能在整個應用程式中進行導覽,並聲明該路徑可將您導向至期望的目的地。

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
...

@Test
fun rallyNavHost_navigateToAllAccounts_callingNavigate() {
    runBlocking {
        withContext(Dispatchers.Main) {
            navController.navigate(RallyScreen.Accounts.name)
        }
    }
    composeTestRule
        .onNodeWithContentDescription("Accounts Screen")
        .assertIsDisplayed()
}

如要進一步瞭解 Compose 中的測試功能,請查看以下步驟中的「後續步驟」一節中所連結的程式碼研究室。

8. 恭喜

恭喜,您已經成功完成本程式碼研究室!

你已將 Navigation 功能新增至 Rally 應用程式,而且現在已瞭解在 Jetpack Compose 使用導覽的相關重要概念。您已瞭解如何建立可組合目的地的導覽圖、新增路徑引數、新增深層連結,以及透過多種方式測試您導入的內容。

後續步驟

查看一些程式碼研究室…

參考文件