Jeptpack Compose 官网教程学习笔记(六)Navigation

Navigation

主要学习内容

  • 将 Jetpack Navigation 与 Jetpack Compose 结合使用的基础知识
  • 在可组合项之间导航
  • 使用必需和可选参数导航
  • 使用深层链接导航
  • 将 TabBar 集成到导航层次结构中
  • 测试导航

准备工作

官网示例下载

因为之后的代码都是基于其中的项目进行的,而且Navigation的学习是基于一个较完善的项目中进行,存在多个界面之间的切换

所以建议下载示例,并通过Import Project方式导入其中的NavigationCodelab项目

在解压文件中的NavigationCodelab 目录中存放本次学习的案例代码

使用Navigation

Rally项目中使用Navigation,遵循以下几个步骤:

  1. 添加Navigation依赖项
  2. 设置NavControllerNavHost
  3. 准备路线
  4. Navigation替换原来的跳转方式

添加依赖项

dependencies {
    ...
    //目前最新稳定版本
    implementation "androidx.navigation:navigation-compose:2.4.2"
}

设置NavController和NavHost

NavController是在 Compose 中使用 Navigation 时的核心组件:可以跟踪组成应用屏幕的可组合项的返回堆栈以及每个屏幕的状态

NavController执行具体的页面切换工作,所以必须先创建它才能进行导航

在 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 可组合项相关联。NavHostNavController 与导航图相关联,导航图用于指定您应能够在其间进行导航的可组合项目的地

可组合项之间进行导航时,NavHost 的内容会自动进行重组。导航图中的每个可组合项目的地都与一个路线相关联

//新建的方法,代码是从RallyApp中copy
//在该方法中修改完成跳转逻辑
@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)) {
            ...   
        }
    }
}

准备路线

Rally应用程序具有三个界面:

  1. 概览 - 所有账户和账单的概览
  2. 账户 - 查看所有账户信息
  3. 账单 - 查看所有账单信息

三个界面都是通过可组合项构建的,我们需要将这些界面映射至Navigaiton目的地中,并且将Overview作为起始目的地

在 Compose 中使用 Navigation 时,路线是一个 String,用于定义指向可组合项的路径。我们可以将其视为指向特定目的地的隐式深层链接。每个目的地都应该有一条唯一的路线

本案例中,我们将使用RallyScreenname属性作为路线

RallyAppWithNavigation中创建的NavHost取代之前的Box,并传入navController。此处以外NavHost还需要一个String类型的startDestination,我们传入RallyScreen.Overview.name。此外,创建一个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中构建导航图

NavHost 创建使用 lambda 来构建导航图。我们可以使用 composable() 方法向导航结构添加内容。此方法需要提供一个路线以及应关联到相应目的地的可组合项:

@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)
            }
        }
    }
}

然而此时运行项目,点击导航栏元素并不会发生界面跳转

用Navigation替换原来的跳转方式

Navigation中实现具体的页面切换工作是NavCotrollerNavCotroller通过navigation()方法进行切换显示的可组合项,使用navigate() 接受代表目的地路线的单个 String 参数

我们需要在RallyTabRowonTabSelected事件中添加跳转逻辑

@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状态不再更新,也就是RallyTabRow内容的选中展开和收合功能不会运转。如果要使得RallyTabRow启用这项功能就需要更新currentScreen状态

不过在Navigation中保留了返回堆栈,并可以将返回堆栈元素作为状态返回,通过这个状态,我们可以对返回堆栈的变更做出反应,比如获取当前的路径

@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"按钮是可点击的,但是不会进行跳转操作

点击无效

OverviewBody可以接受几个函数作为点击事件的回调。我们实现onClickSeeAllAccountsonClickSeeAllBills实现导航到相关目的地

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

示例中大量使用单向数据流,通过事件上传、状态下流的做法,提供了可组合项的复用性

参数导航

Navigation Compose 还支持在可组合项目的地之间传递参数。为此,您需要向路线中添加参数占位符

参数导航使得路线动态化,通过将一个或多个参数传递到路由并调整参数类型或默认值来使路由行为动态化

我们通过为Rally增加点击账户,跳转至显示单个帐户的详细信息界面来学习该内容

NavHost中添加新的路线 ("$accountsName/{name}") ,同时我们还要指定传递参数的类型

@Composable
fun RallyAppWithNavigation() {
    ...
    Scaffold(
        ...
    ) { innerPadding ->
        NavHost(
            navController = navController,
            startDestination = RallyScreen.Overview.name,
            modifier = Modifier.padding(innerPadding)
        ) {
            ...
            val accountsName = RallyScreen.Accounts.name
            composable("$accountsName/{name}",
                //传递的参数可能不止一个,所以使用List集合
                arguments = listOf(
                    //名字为name的参数类型为String
                    navArgument("name") {
                        type = NavType.StringType
                    }
                )
            ) { ... }
        }
    }
}

通过向路线中添加参数占位符的方式传递参数,如上所示$accountsName/{name}

使用美元符号$来转义变量名,{argument}表示一个变量

composable会接收到NavBackStackEntry对象,NavBackStackEntry会根据指定的路径和参数对进行分析

我们可以使用NavBackStackEntry获取参数值,即name,然后根据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 还支持可选的导航参数。可选参数与必需参数有以下两点不同:

  • 可选参数必须使用查询参数语法 ("?argName={argName}") 来添加
  • 可选参数必须具有 defaultValue 集或 nullable = true(将默认值隐式设置为 null

多个可选参数之间用&连接,中间不能存在空格

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")
//可选类型可以调换顺序
navController.navigate("$accountsName/$name?arg2=20&arg1=10")

默认情况下,所有参数都会被解析为字符串

导航至SingleAccountBody

若要将参数传递到目的地,我们需要在 navigate 中根据参数位置填写具体的值

我们需要在OverviewBody中的onAccountClickAccountsBody中的onAccountClick事件中添加跳转逻辑

@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")
                    })
            }
            ...
        }
    }
}

此时运行应用程序时,单击每个帐户并将进入一个屏幕,显示给定帐户的数据

深层链接

除了参数导航之外,您还可以使用 深层链接 将应用中的目标公开给第三方应用

添加 intent-filter

首先,将深层链接添加至 AndroidManifest.xml,我们需要使用VIEW建立RallyActivity的意图选择器,并指定类别为BROWSABLEDEFAULT

然后使用data标签中指定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}参数

回应深层链接

现在我们可以在RallyActivity中回应传入的意图

composable中使用 navDeepLink函数增加deepLinks参数,在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")
        })
}

我们可以在当前应用或别的应用中使用深层链接方式跳转至该页面

当前应用使用深层链接

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

其他应用使用深层链接

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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容