Jetpack Compose中的Navigation从入门到精通完全指南
什么是Android导航
导航帮助您理解应用程序在不同组件间的移动方式。
Android JetPack Navigation可以帮助您以简单的方式实现高级导航。
导航组件由三个主要部分组成:
导航图(Navigation Graph): 这是一个资源,它将所有与导航相关的数据集中在一起。这包括应用程序中的所有位置(称为目标位置)以及用户在应用程序中可能采取的路径。它就像一本大书,里面包含了您在应用程序中可以访问的所有地方以及如何在它们之间移动的方法。可以将其视为地图和指南的结合。
NavHost: 这是一个独特的可组合项,您可以将其包含在布局中。它显示来自您的导航图的各个目标位置。NavHost将NavController与指定可组合项目标位置的导航图链接起来。当您在可组合项之间导航时,NavHost的内容将自动重新组合。导航图中的每个可组合项目标位置都与一个路由关联。
NavController: NavController是导航组件的中央API。它具有状态并跟踪组成应用程序屏幕的可组合项的后退栈以及每个屏幕的状态。
Jetpack Compose中的导航
导航组件为Jetpack Compose应用程序提供了支持。您可以在利用导航组件的基础设施和功能的同时,在可组合项之间进行导航。
要开始在Jetpack Compose中进行导航,您需要在项目的build.gradle文件中包含所需的依赖项:
implementation "androidx.navigation:navigation-compose:2.7.1"
Jetpack Compose中导航的基本概念。
NavController:
NavController是导航组件的中央API。它具有状态并跟踪组成应用程序屏幕的可组合项的后退栈以及每个屏幕的状态。
您可以通过在可组合项中使用rememberNavController()
方法来创建一个NavController:
val navController = rememberNavController()
您应该在可组合项层次结构中的一个位置创建NavController,并确保所有需要引用它的可组合项都能够访问它。这遵循状态提升的原则,并允许您使用NavController和通过currentBackStackEntryAsState()
提供的状态作为更新屏幕外的可组合项的真实数据源。有关此功能的示例,请参阅与底部导航栏集成。
注意:如果您在片段中使用导航组件,您无需在Compose中定义新的导航图或使用NavHost可组合项。有关更多信息,请参阅互操作性部分。
NavHost:
每个NavController
必须与一个NavHost
可组合项关联。NavHost将NavController
与指定应该能够在其之间导航的可组合项目标位置的导航图链接起来。当您在可组合项之间导航时,NavHost
的内容将自动重新组合。导航图中的每个可组合项目标位置都与一个路由相关联。
关键术语:路由是一个字符串,用于定义到达特定可组合项的路径。可以将其视为指向特定目的地的隐式深层链接。每个目的地应具有唯一的路由。
创建NavHost需要之前通过rememberNavController()
创建的NavController
以及您的导航图的起始目的地的路由。NavHost的创建使用了导航Kotlin DSL中的lambda语法来构建导航图。您可以使用composable()
方法来添加导航结构。该方法要求您提供一个路由和应与目的地关联的可组合项:
NavHost(navController = navController, startDestination = "profile") {
composable("profile") { Profile(/*...*/) }
composable("friendslist") { FriendsList(/*...*/) }
/*...*/
}
注意:导航组件要求您遵循导航原则并使用固定的起始目的地。您不应该对
startDestination
路由使用可组合项值。
示例:如何设置导航图、NavHost
和NavigationItem
。
步骤1:在一个文件中定义导航的屏幕名称和路由,例如AppNavigation.kt
。
enum class Screen {
HOME,
LOGIN,
}
sealed class NavigationItem(val route: String) {
object Home : NavigationItem(Screen.HOME.name)
object Login : NavigationItem(Screen.LOGIN.name)
}
步骤2:定义NavHost
,比如AppNavHost.kt
@Composable
fun AppNavHost(
modifier: Modifier = Modifier,
navController: NavHostController,
startDestination: String = NavigationItem.Splash.route,
... // other parameters
) {
NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
composable(NavigationItem.Splash.route) {
SplashScreen(navController)
}
composable(NavigationItem.Login.route) {
LoginScreen(navController)
}
}
步骤3:在你的MainActivity.kt
中调用AppNavHost
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AutoPartsAppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
AppNavHost(navController = rememberNavController())
}
}
}
}
}
导航参数:
导航Compose还支持在可组合项目标位置之间传递参数。为了实现这一点,您需要向路由添加参数占位符,类似于在使用基础导航库时向深层链接添加参数的方式:
使用情况:
- 无参数
- 使用简单参数,如预定义的数据类型(例如Int、String等)
- 使用复杂参数,如用户定义的数据类型
- 可选参数
- 返回结果的导航
无参数:
NavHost(navController = navController, startDestination = "profile") {
composable("profile") { Profile(/*...*/) }
composable("friendslist") { FriendsList(/*...*/) }
/*...*/
}
使用简单参数:
默认情况下,所有参数都会被解析为字符串。composable()
的arguments参数接受NamedNavArguments
列表。您可以使用navArgument
方法快速创建NamedNavArgument
,并指定其确切类型:
NavHost(startDestination = "profile/{userId}") {
...
composable("profile/{userId}") {...}
}
默认情况下,所有参数都会被解析为字符串。composable()
的arguments参数接受NamedNavArguments
列表。您可以使用navArgument
方法快速创建NamedNavArgument
,并指定其确切类型:
NavHost(startDestination = "profile/{userId}") {
...
composable(
"profile/{userId}",
arguments = listOf(navArgument("userId"){
type = NavType.StringType
})
) {...}
}
您应该从composable()
函数的lambda中可用的NavBackStackEntry
中提取参数。
composable("profile/{userId}") { backStackEntry ->
val userId = backStackEntry.arguments?.getString("userId")
// 在这里获取用户数据
Profile(
navController,
// 传递获取的用户数据,如UserInfo
)
}
要将参数传递给目标位置,需要在进行导航调用时将其附加到路由中:
navController.navigate("profile/user1234")
有关支持的类型列表,请参阅在目的地之间传递数据。
使用复杂参数或用户定义的参数:
强烈建议在导航时不要传递复杂的数据对象,而是在执行导航操作时将最小必要的信息作为参数传递,例如唯一标识符或其他形式的ID:
// 仅在导航到新目标时传递用户ID作为参数
navController.navigate("profile/user1234")
复杂对象应存储为单一真相源(例如数据层)中的数据。导航到目的地后,您可以使用传递的ID从单一真相源中加载所需信息。要检索ViewModel中的参数,负责访问数据层的ViewModel,您可以使用ViewModel的SavedStateHandle
:
class UserViewModel(
savedStateHandle: SavedStateHandle,
private val userInfoRepository: UserInfoRepository
) : ViewModel() {
private val userId: String = checkNotNull(savedStateHandle["userId"])
// Fetch the relevant user information from the data layer,
// ie. userInfoRepository, based on the passed userId argument
private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(userId)
--------------- OR -----------------
// fetch data from network or database
private val _dataFlow =
MutableStateFlow<UserInfo>(userInfoRepository.getUserInfo(userId))
val dataFlow get() = _dataFlow.asStateFlow()
}
Composable
函数
//Navhost
composable("profile/{userId}") { backStackEntry ->
val userId = backStackEntry.arguments?.getString("userId")
// here you have to fetch user data
val userInfo by taskViewModel.dataFlow.collectAsState()
Profile(
navController,
userInfo
)
}
// Profile screen
@Composable
fun Profile(navController: NavController, userInfo:UserInfo){
// do you work here
}
注意:将ViewModel放在可组合屏幕之外,因为预览将无法工作,并且最佳实践是避免可组合和ViewModel之间的耦合。
这种方法有助于防止在配置更改期间数据丢失以及更新或改变对象时出现任何不一致性。
有关为什么应避免将复杂数据作为参数传递的更详细解释,以及支持的参数类型列表,请参阅在目的地之间传递数据。
添加可选参数
Navigation Compose还支持可选导航参数。可选参数与必需参数有两点不同:
必须使用查询参数语法("?argName={argName}"
)进行包含
必须设置defaultValue
,或者设置nullable = true
(隐式地将默认值设置为null)
这意味着所有可选参数都必须显式地添加到composable()
函数中作为列表:
composable(
"profile?userId={userId}/{isMember}",
arguments = listOf(
navArgument("userId") {
type = NavType.StringType
defaultValue = "user1234"
// OR
type = NavType.StringType
nullable = true
},
navArgument("isNewTask") {
type = NavType.BoolType
}
)
) { backStackEntry ->
val userId = backStackEntry.arguments?.getString("userId")
val isMember = backStackEntry.arguments?.getBoolean("isMember")?:false
Profile(navController, userId, isMember)
}
现在,即使没有传递参数给目标,defaultValue = "user1234"
也会被使用。
通过路由处理参数的结构意味着您的可组合部件完全独立于导航,并使它们更容易进行测试。
带有结果的返回导航
带有结果的返回导航是最常见的任务。例如,当您打开筛选对话框并选择筛选条件,然后带着所选的筛选条件返回导航到应用这些筛选条件到屏幕上。
有两个屏幕。1. FirstScreen
(第一个屏幕)和2. SecondScreen
(第二个屏幕)。我们需要从SecondScreen
(第二个屏幕)获取数据到FirstScreen
(第一个屏幕)。
NavHost.kt:设置导航图。
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "firstscreen"
) {
composable("firstscreen") {
FirstScreen(navController)
}
composable("secondscreen") {
SecondScreen(navController)
}
}
@Composable
fun FirstScreen(navController: NavController) {
// Retrieve data from next screen
val msg =
navController.currentBackStackEntry?.savedStateHandle?.get<String>("msg")
Column(
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Button(onClick = { navController.navigate("secondscreen") }) {
Text("Go to next screen")
}
Spacer(modifier = Modifier.height(8.dp))
msg?.let {
Text(it)
}
}
}
FirstScreen.kt: 利用NavController当前返回堆栈条目的savedStateHandle
在从SecondScreen
返回后检索数据。
@Composable
fun FirstScreen(navController: NavController) {
// Retrieve data from next screen
val msg =
navController.currentBackStackEntry?.savedStateHandle?.get<String>("msg")
Column(
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Button(onClick = { navController.navigate("secondscreen") }) {
Text("Go to next screen")
}
Spacer(modifier = Modifier.height(8.dp))
msg?.let {
Text(it)
}
}
}
SecondScreen.kt: 将数据放入NavController
的前一个返回堆栈条目的savedStateHandle
中。
@Composable
fun SecondScreen(navController: NavController) {
var text by remember {
mutableStateOf("")
}
Column(
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
TextField(
value = text, onValueChange = { text = it },
placeholder = {
Text("Enter text", color = Color.Gray)
}
)
Spacer(Modifier.height(8.dp))
Button(onClick = {
// Put data into savedStateHandle to retrive data on the previous screen
navController.previousBackStackEntry?.savedStateHandle?.set("msg", text)
navController.popBackStack()
}) {
Text(text = "Submit")
}
}
}
https://github.com/KaushalVasava/JetPackCompose_Basic/tree/navigate-back-with-result
Deep links
Navigation Compose支持将隐式深链接定义为composable()函数的一部分。它的deepLinks参数接受一个NavDeepLinks列表,可以使用navDeepLink方法快速创建:
val uri = "https://www.example.com"
composable(
"profile?id={id}",
deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("id"))
}
这些深链接可以将特定的URL、操作或MIME类型与可组合关联起来。默认情况下,这些深链接不会向外部应用程序公开。要使这些深链接对外部可用,必须在应用程序的manifest.xml文件中添加适当的元素。为了启用上述深链接,您应该在清单文件的元素内添加以下内容:
<activity …>
<intent-filter>
...
<data android:scheme="https" android:host="www.example.com" />
</intent-filter>
</activity>
当深链接被其他应用触发时,导航会自动进入到该可组合。
这些相同的深链接也可以用于从可组合中构建具有适当深链接的PendingIntent:
val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
Intent.ACTION_VIEW,
"https://www.example.com/$id".toUri(),
context,
MyActivity::class.java
)
val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(deepLinkIntent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
然后,您可以像其他任何PendingIntent一样使用这个deepLinkPendingIntent来打开您的应用程序的深链接目标位置。
Nested Navigation
目标可以分组为嵌套图形,以模块化您应用程序界面中的特定流程。一个例子就是一个独立的登录流程。
嵌套图形将其目标分组,就像主图形一样,并且它需要一个指定的起始目标作为其关联路由,当访问该嵌套图形的路由时,您将进入该起始目标。
要将嵌套图形添加到NavHost中,可以使用导航扩展函数:
NavHost(navController, startDestination = "home") {
...
// 通过其路由('login')导航到图形会自动导航到图形的起始目标 - 'username'
// 因此封装了图形的内部路由逻辑
navigation(startDestination = "username", route = "login") {
composable("username") { ... }
composable("password") { ... }
composable("registration") { ... }
}
...
}
强烈建议随着图形的增长,将导航图形拆分为多个方法。这还允许多个模块贡献自己的导航图形。
fun NavGraphBuilder.loginGraph(navController: NavController) {
navigation(startDestination = "username", route = "login") {
composable("username") { ... }
composable("password") { ... }
composable("registration") { ... }
}
}
通过将该方法作为NavGraphBuilder的扩展方法,您可以与预构建的导航、composable和dialog扩展方法一起使用:
NavHost(navController, startDestination = "home") {
...
loginGraph(navController)
...
}
示例:
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home") {
composable("about") {}
navigation(
startDestination = "login",
route = "auth"
) {
composable("login") {
val viewModel = it.sharedViewModel<SampleViewModel>(navController)
Button(onClick = {
navController.navigate("calendar") {
popUpTo("auth") {
inclusive = true
}
}
}) {
}
}
composable("register") {
val viewModel = it.sharedViewModel<SampleViewModel>(navController)
}
composable("forgot_password") {
val viewModel = it.sharedViewModel<SampleViewModel>(navController)
}
}
navigation(
startDestination = "calendar_overview",
route = "calendar"
) {
composable("calendar_overview") { }
composable("calendar_entry") { }
}
}
NavBackStack
扩展函数
@Composable
inline fun <reified T : ViewModel> NavBackStackEntry.sharedViewModel(navController: NavController): T {
val navGraphRoute = destination.parent?.route ?: return viewModel()
val parentEntry = remember(this) {
navController.getBackStackEntry(navGraphRoute)
}
return viewModel(parentEntry)
}
与bottom nav bar的集成
通过在可组合层次结构的较高级别定义NavController
,您可以将导航与其他组件(如底部导航组件)连接起来。这样做可以通过选择底部栏中的图标进行导航。
要使用BottomNavigation
和BottomNavigationItem
组件,请将androidx.compose.material
依赖项添加到Android应用程序中。
dependencies {
implementation "androidx.compose.material:material:1.5.1"
}
android {
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.3"
}
kotlinOptions {
jvmTarget = "1.8"
}
}
为了将底部导航栏中的项链接到导航图中的路由,建议定义一个sealed class(例如这里的Screen),其中包含目标的路由和String资源ID。
sealed class Screen(val route: String, @StringRes val resourceId: Int) {
object Profile : Screen("profile", R.string.profile)
object FriendsList : Screen("friendslist", R.string.friends_list)
}
然后将这些项放入可以供BottomNavigationItem使用的列表中:
val items = listOf(
Screen.Profile,
Screen.FriendsList,
)
在BottomNavigation
的可组合函数中,使用currentBackStackEntryAsState()
函数获取当前的NavBackStackEntry
。该条目可让您访问当前的NavDestination
。然后,可以通过比较项的路由与当前目标及其父目标的路由(以处理使用嵌套导航时的情况)来确定每个BottomNavigationItem
的选中状态,通过NavDestination
层次结构。
项的路由还用于将onClick lambda连接到导航调用,以便点击该项时导航到该项。通过使用saveState
和restoreState
标志,正确保存和恢复该项的状态和返回堆栈,以便在底部导航项之间切换时进行操作。
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
items.forEach { screen ->
BottomNavigationItem(
icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
label = { Text(stringResource(screen.resourceId)) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
}
}
}
) { innerPadding ->
NavHost(navController, startDestination = Screen.Profile.route, Modifier.padding(innerPadding)) {
composable(Screen.Profile.route) { Profile(navController) }
composable(Screen.FriendsList.route) { FriendsList(navController) }
}
}
在这里,您可以利用NavController.currentBackStackEntryAsState()
方法来提升navController
状态,将其从NavHost函数中提取出来,并与BottomNavigation
组件共享。这意味着BottomNavigation
会自动具有最新的状态。