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
): 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 = emptyList(),
    deepLinks: List = 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}的格式作为深层链接地址


    
        
        
    
    
        
        
        
        
    

不需要在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

你可能感兴趣的:(Jeptpack Compose 官网教程学习笔记(六)Navigation)