Jeptpack Compose 官網(wǎng)教程學(xué)習(xí)筆記(六)Navigation

Navigation

主要學(xué)習(xí)內(nèi)容

  • 將 Jetpack Navigation 與 Jetpack Compose 結(jié)合使用的基礎(chǔ)知識
  • 在可組合項之間導(dǎo)航
  • 使用必需和可選參數(shù)導(dǎo)航
  • 使用深層鏈接導(dǎo)航
  • 將 TabBar 集成到導(dǎo)航層次結(jié)構(gòu)中
  • 測試導(dǎo)航

準(zhǔn)備工作

官網(wǎng)示例下載

因為之后的代碼都是基于其中的項目進(jìn)行的,而且Navigation的學(xué)習(xí)是基于一個較完善的項目中進(jìn)行铸敏,存在多個界面之間的切換

所以建議下載示例属桦,并通過Import Project方式導(dǎo)入其中的NavigationCodelab項目

在解壓文件中的NavigationCodelab 目錄中存放本次學(xué)習(xí)的案例代碼

使用Navigation

Rally項目中使用Navigation餐胀,遵循以下幾個步驟:

  1. 添加Navigation依賴項
  2. 設(shè)置NavControllerNavHost
  3. 準(zhǔn)備路線
  4. Navigation替換原來的跳轉(zhuǎn)方式

添加依賴項

dependencies {
    ...
    //目前最新穩(wěn)定版本
    implementation "androidx.navigation:navigation-compose:2.4.2"
}

設(shè)置NavController和NavHost

NavController是在 Compose 中使用 Navigation 時的核心組件:可以跟蹤組成應(yīng)用屏幕的可組合項的返回堆棧以及每個屏幕的狀態(tài)

NavController執(zhí)行具體的頁面切換工作,所以必須先創(chuàng)建它才能進(jìn)行導(dǎo)航

在 Compose 中熟尉,我們通過使用 rememberNavController()獲取到NavHostController實例

NavHostController是``NavController`的子類

@Composable
public fun rememberNavController(
    vararg navigators: Navigator<out NavDestination>
): NavHostController {
    val context = LocalContext.current
    //可以看到 NavHostController 還是具有保存功能的
    return rememberSaveable(inputs = navigators, saver = NavControllerSaver(context)) {
        createNavController(context)
    }.apply {
        for (navigator in navigators) {
            navigatorProvider.addNavigator(navigator)
        }
    }
}

每個NavController 都必須與一個 NavHost 可組合項相關(guān)聯(lián)蜈垮。NavHostNavController 與導(dǎo)航圖相關(guān)聯(lián)雕什,導(dǎo)航圖用于指定您應(yīng)能夠在其間進(jìn)行導(dǎo)航的可組合項目的地

可組合項之間進(jìn)行導(dǎo)航時,NavHost 的內(nèi)容會自動進(jìn)行重組苹粟。導(dǎo)航圖中的每個可組合項目的地都與一個路線相關(guān)聯(lián)

//新建的方法有滑,代碼是從RallyApp中copy
//在該方法中修改完成跳轉(zhuǎn)邏輯
@Composable
fun RallyAppWithNavigation() {
    val allScreens = RallyScreen.values().toList()
    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 = ""){}
       
        Box(Modifier.padding(innerPadding)) {
            ...   
        }
    }
}

準(zhǔn)備路線

Rally應(yīng)用程序具有三個界面:

  1. 概覽 - 所有賬戶和賬單的概覽
  2. 賬戶 - 查看所有賬戶信息
  3. 賬單 - 查看所有賬單信息

三個界面都是通過可組合項構(gòu)建的,我們需要將這些界面映射至Navigaiton目的地中嵌削,并且將Overview作為起始目的地

在 Compose 中使用 Navigation 時毛好,路線是一個 String望艺,用于定義指向可組合項的路徑。我們可以將其視為指向特定目的地的隱式深層鏈接肌访。每個目的地都應(yīng)該有一條唯一的路線

本案例中荣茫,我們將使用RallyScreenname屬性作為路線

RallyAppWithNavigation中創(chuàng)建的NavHost取代之前的Box,并傳入navController场靴。此處以外NavHost還需要一個String類型的startDestination啡莉,我們傳入RallyScreen.Overview.name。此外旨剥,創(chuàng)建一個Modifier將填充傳遞到NavHost

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

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

NavHost方法

@Composable
public fun NavHost(
    navController: NavHostController,
    startDestination: String,
    modifier: Modifier = Modifier,
    route: String? = null,
    builder: NavGraphBuilder.() -> Unit
) { ... }
  • navControllerNavHostController實例對象
  • startDestination :起始目的地
  • builder:在NavGraphBuilder中構(gòu)建導(dǎo)航圖

NavHost 創(chuàng)建使用 lambda 來構(gòu)建導(dǎo)航圖咧欣。我們可以使用 composable() 方法向?qū)Ш浇Y(jié)構(gòu)添加內(nèi)容。此方法需要提供一個路線以及應(yīng)關(guān)聯(lián)到相應(yīng)目的地的可組合項:

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

    Scaffold( ... ) { innerPadding ->
        NavHost(
            navController = navController,
            startDestination = RallyScreen.Overview.name,
            modifier = Modifier.padding(innerPadding)
        ) {
            composable(RallyScreen.Overview.name){
                OverviewBody()
            }
            composable(RallyScreen.Accounts.name){
                AccountsBody(accounts = UserData.accounts)
            }
            composable(RallyScreen.Bills.name){
                BillsBody(bills = UserData.bills)
            }
        }
    }
}

然而此時運行項目轨帜,點擊導(dǎo)航欄元素并不會發(fā)生界面跳轉(zhuǎn)

用Navigation替換原來的跳轉(zhuǎn)方式

Navigation中實現(xiàn)具體的頁面切換工作是NavCotroller魄咕,NavCotroller通過navigation()方法進(jìn)行切換顯示的可組合項,使用navigate() 接受代表目的地路線的單個 String 參數(shù)

我們需要在RallyTabRowonTabSelected事件中添加跳轉(zhuǎn)邏輯

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

    Scaffold(
        topBar = {
            RallyTabRow(
                allScreens = allScreens,
                onTabSelected = { screen ->
                    //currentScreen = screen
                    navController.navigate(currentScreen.name)
                },
                currentScreen = currentScreen
            )
        }
    ) { innerPadding ->
        ...
    }
}

修改onTabSelected事件后蚌父,currentScreen狀態(tài)不再更新哮兰,也就是RallyTabRow內(nèi)容的選中展開和收合功能不會運轉(zhuǎn)。如果要使得RallyTabRow啟用這項功能就需要更新currentScreen狀態(tài)

不過在Navigation中保留了返回堆棧苟弛,并可以將返回堆棧元素作為狀態(tài)返回喝滞,通過這個狀態(tài),我們可以對返回堆棧的變更做出反應(yīng)膏秫,比如獲取當(dāng)前的路徑

@Composable
fun RallyAppWithNavigation() {
    val allScreens = RallyScreen.values().toList()
    val navController = rememberNavController()
    
    val backstackEntry by navController.currentBackStackEntryAsState()
    var currentScreen =
        RallyScreen.fromRoute(backstackEntry?.destination?.route ?: RallyScreen.Overview.name)

    Scaffold(
        topBar = {
            RallyTabRow(
                allScreens = allScreens,
                onTabSelected = { screen ->
                    navController.navigate(screen.name)
                },
                currentScreen = currentScreen
            )
        }
    ) { innerPadding ->
        NavHost(
            navController = navController,
            startDestination = RallyScreen.Overview.name,
            modifier = Modifier.padding(innerPadding)
        ) {
            ...
        }
    }
}

啟用OverviewScreen的點擊

在此項目中右遭,OverviewBody忽略了點擊事件,其中的"SEE ALL"按鈕是可點擊的缤削,但是不會進(jìn)行跳轉(zhuǎn)操作

點擊無效

OverviewBody可以接受幾個函數(shù)作為點擊事件的回調(diào)窘哈。我們實現(xiàn)onClickSeeAllAccountsonClickSeeAllBills實現(xiàn)導(dǎo)航到相關(guān)目的地

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

示例中大量使用單向數(shù)據(jù)流,通過事件上傳亭敢、狀態(tài)下流的做法滚婉,提供了可組合項的復(fù)用性

參數(shù)導(dǎo)航

Navigation Compose 還支持在可組合項目的地之間傳遞參數(shù)。為此帅刀,您需要向路線中添加參數(shù)占位符

參數(shù)導(dǎo)航使得路線動態(tài)化让腹,通過將一個或多個參數(shù)傳遞到路由并調(diào)整參數(shù)類型或默認(rèn)值來使路由行為動態(tài)化

我們通過為Rally增加點擊賬戶,跳轉(zhuǎn)至顯示單個帳戶的詳細(xì)信息界面來學(xué)習(xí)該內(nèi)容

NavHost中添加新的路線 ("$accountsName/{name}") 劝篷,同時我們還要指定傳遞參數(shù)的類型

@Composable
fun RallyAppWithNavigation() {
    ...
    Scaffold(
        ...
    ) { innerPadding ->
        NavHost(
            navController = navController,
            startDestination = RallyScreen.Overview.name,
            modifier = Modifier.padding(innerPadding)
        ) {
            ...
            val accountsName = RallyScreen.Accounts.name
            composable("$accountsName/{name}",
                //傳遞的參數(shù)可能不止一個哨鸭,所以使用List集合
                arguments = listOf(
                    //名字為name的參數(shù)類型為String
                    navArgument("name") {
                        type = NavType.StringType
                    }
                )
            ) { ... }
        }
    }
}

通過向路線中添加參數(shù)占位符的方式傳遞參數(shù),如上所示$accountsName/{name}

使用美元符號$來轉(zhuǎn)義變量名娇妓,{argument}表示一個變量

composable會接收到NavBackStackEntry對象像鸡,NavBackStackEntry會根據(jù)指定的路徑和參數(shù)對進(jìn)行分析

我們可以使用NavBackStackEntry獲取參數(shù)值,即name,然后根據(jù)name查找到UserData并傳遞給SingleAccountBody可組合項

val accountsName = RallyScreen.Accounts.name
composable("$accountsName/{name}",
    arguments = listOf(
        navArgument("name") {
            type = NavType.StringType
        }
    )
) { backStackEntry ->
    val accountName = backStackEntry.arguments?.getString("name")
    val account = UserData.getAccount(accountName)
    SingleAccountBody(account = account)
}

composable方法代碼

public fun NavGraphBuilder.composable(
    route: String,
    arguments: List<NamedNavArgument> = emptyList(),
    deepLinks: List<NavDeepLink> = emptyList(),
    content: @Composable (NavBackStackEntry) -> Unit
)

Navigation Compose 還支持可選的導(dǎo)航參數(shù)只估≈救海可選參數(shù)與必需參數(shù)有以下兩點不同:

  • 可選參數(shù)必須使用查詢參數(shù)語法 ("?argName={argName}") 來添加
  • 可選參數(shù)必須具有 defaultValue 集或 nullable = true(將默認(rèn)值隱式設(shè)置為 null

多個可選參數(shù)之間用&連接,中間不能存在空格

composable("$accountsName/{name}?arg1={arg1}&arg2={arg2}",
    arguments = listOf(
        navArgument("name") {
            type = NavType.StringType
        },
        navArgument("arg1") {
            defaultValue = 100
            type = NavType.IntType
        },
        navArgument("arg2") {
            defaultValue = 200
            type = NavType.IntType
        }
    )
)
navController.navigate("$accountsName/$name?arg2=20")
//可選類型可以調(diào)換順序
navController.navigate("$accountsName/$name?arg2=20&arg1=10")

默認(rèn)情況下蛔钙,所有參數(shù)都會被解析為字符串

導(dǎo)航至SingleAccountBody

若要將參數(shù)傳遞到目的地锌云,我們需要在 navigate 中根據(jù)參數(shù)位置填寫具體的值

我們需要在OverviewBody中的onAccountClickAccountsBody中的onAccountClick事件中添加跳轉(zhuǎn)邏輯

@Composable
fun RallyAppWithNavigation() {
    ...
    Scaffold(
        ...
    ) { innerPadding ->
        NavHost(
            navController = navController,
            startDestination = RallyScreen.Overview.name,
            modifier = Modifier.padding(innerPadding)
        ) {
            val accountsName = RallyScreen.Accounts.name

            composable(RallyScreen.Overview.name) {
                OverviewBody(onAccountClick = { name ->
                    navController.navigate("$accountsName/$name")
                })
            }
            composable(RallyScreen.Accounts.name) {
                AccountsBody(accounts = UserData.accounts,
                    onAccountClick = { name ->
                        navController.navigate("$accountsName/$name")
                    })
            }
            ...
        }
    }
}

此時運行應(yīng)用程序時,單擊每個帳戶并將進(jìn)入一個屏幕吁脱,顯示給定帳戶的數(shù)據(jù)

深層鏈接

除了參數(shù)導(dǎo)航之外桑涎,您還可以使用 深層鏈接 將應(yīng)用中的目標(biāo)公開給第三方應(yīng)用

添加 intent-filter

首先,將深層鏈接添加至 AndroidManifest.xml兼贡,我們需要使用VIEW建立RallyActivity的意圖選擇器攻冷,并指定類別為BROWSABLEDEFAULT

然后使用data標(biāo)簽中指定schemehost

這個intent-filter會使用rally://accounts/{name}的格式作為深層鏈接地址

<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>

不需要在AndroidManifest.xml中申明{name}參數(shù)

回應(yīng)深層鏈接

現(xiàn)在我們可以在RallyActivity中回應(yīng)傳入的意圖

composable中使用 navDeepLink函數(shù)增加deepLinks參數(shù),在navDeepLink中將uriPattern賦值為符合intent-filter的格式

composable(route = RallyScreen.Accounts.name,
    //深層鏈接格式可以存在多個
    deepLinks = listOf(navDeepLink {
        uriPattern = "rally://accounts/{name}"
    })
) {
    AccountsBody(accounts = UserData.accounts,
        onAccountClick = { name ->
            navController.navigate("$accountsName/$name")
        })
}

我們可以在當(dāng)前應(yīng)用或別的應(yīng)用中使用深層鏈接方式跳轉(zhuǎn)至該頁面

當(dāng)前應(yīng)用使用深層鏈接

navController.navigate(Uri.parse("rally://accounts/$name"))

其他應(yīng)用使用深層鏈接

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        Surface {
            DeepLinkNavigationButton{
                val deepLinkIntent = Intent()
                deepLinkIntent.data="rally://accounts/Checking".toUri()
                deepLinkIntent.flags= Intent.FLAG_ACTIVITY_NEW_TASK
                startActivity(deepLinkIntent)
            }
        }
    }
}

@Composable
fun DeepLinkNavigationButton(onClick:()->Unit) {
    Button(onClick = {
        try{
            onClick()
        }catch (e:Exception){
            Log.e("navigation", "exception: ${e.message}" )
            e.printStackTrace()
        }
    }){
        Text(text = "深度鏈接")
    }    
}

也可以在模擬器上使用 ADB 來測試深層鏈接

adb shell am start -d "rally://accounts/Checking" -a android.intent.action.VIEW
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末遍希,一起剝皮案震驚了整個濱河市等曼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌凿蒜,老刑警劉巖禁谦,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異废封,居然都是意外死亡州泊,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進(jìn)店門虱饿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拥诡,“玉大人,你說我怎么就攤上這事氮发。” “怎么了冗懦?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵爽冕,是天一觀的道長。 經(jīng)常有香客問我披蕉,道長颈畸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任没讲,我火速辦了婚禮眯娱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘爬凑。我一直安慰自己徙缴,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布嘁信。 她就那樣靜靜地躺著于样,像睡著了一般疏叨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上穿剖,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天蚤蔓,我揣著相機與錄音,去河邊找鬼糊余。 笑死秀又,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的贬芥。 我是一名探鬼主播吐辙,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼誓军!你這毒婦竟也來了袱讹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤昵时,失蹤者是張志新(化名)和其女友劉穎捷雕,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體壹甥,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡救巷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了句柠。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片浦译。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖溯职,靈堂內(nèi)的尸體忽然破棺而出精盅,到底是詐尸還是另有隱情,我是刑警寧澤谜酒,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布叹俏,位于F島的核電站,受9級特大地震影響僻族,放射性物質(zhì)發(fā)生泄漏粘驰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一述么、第九天 我趴在偏房一處隱蔽的房頂上張望蝌数。 院中可真熱鬧,春花似錦度秘、人聲如沸顶伞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽枝哄。三九已至肄梨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間挠锥,已是汗流浹背众羡。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蓖租,地道東北人粱侣。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像蓖宦,于是被迫代替她去往敵國和親齐婴。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容