【Android】Navigation

发布时间:2026/11/6 4:59:40
【Android】Navigation 一. Navigation1.1. 导航组件的组成部分Navigation组件有主要的三个部分NaviController:负责在目标页面即应用中的屏幕之间导航。NavGraph用于映射要导航到的可组合项目标页面。NavHost此可组合项充当容器用于显示NavGraph的当前目标页面。1.2. 在应用中为目标页面定义路线在 Compose 应用中导航的一个基本概念就是路线。路线是与目标页面对应的字符串。这类似于网址的概念。就像不同网址映射到网站上的不同页面一样路线是可映射至目标页面并作为其唯一标识符的字符串。应用中的屏幕数量有限因此路线数量也是有限的。您可以使用枚举类来定义应用的路线。Kotlin 中的枚举类具有一个名称属性该属性会返回具有属性名称的字符串。1.2.1 定义枚举首先我们需要定义一个枚举来定义路线名称:enumclassCupcakeScreen(){Start,Flavor,Pickup,Summary}1.2.2. 添加NavHostNavHost 是一个可组合项用于根据给定路线来显示其他可组合项目标页面。例如如果路线为 FlavorNavHost会显示用于选择纸杯蛋糕口味的屏幕。如果路线为 Summary则应用会显示摘要屏幕。NavHost的语法和其他可组合项的语法一样这里有两个参数是需要注意navControllerNavHostController类的实例。您可以使用此对象在屏幕之间导航。例如通过调用navigate()方法导航到另一个目标页面。您可以通过从可组合函数调用rememberNavController()来获取NavHostController。startDestination 此字符串路线用于定义应用首次显示NavHost时默认显示的目标页面。对于 Cupcake 应用这应该是 Start 路线。与其他可组合项一样NavHost也接受modifier参数注意NavHostController是NavController类的子类可提供与 NavHost 可组合项搭配使用的额外功能。1.2.3. NavHost中处理路线与其他可组合项一样NavHost 接受函数类型作为其内容。在NavHost的内容函数中调用composable()函数。composable()函数有两个必需参数。route 与路线名称对应的字符串。这可以是任何唯一的字符串。您将使用CupcakeScreen第一步定义的枚举枚举的常量的名称属性。content 您可以在此处调用要为特定路线显示的可组合项。您将针对这四个路线分别调用一次 composable() 函数。注意composable()函数是NavGraphBuilder的扩展函数。1.2.4. NavHost完成实例代码添加NavHost的实例代码// app起始函数ComposablefunCupcakeApp(viewModel:OrderViewModelOrderViewModel(),// 管理数据的viewModel对象navController:NavHostControllerrememberNavController()// NavController的实例// 监听当前页面变化页面切换时自动更新valnavBackStackEntrybynavController.currentBackStackEntryAsState()// 当前页面的路由名称valcurrentRoutenavBackStackEntry.destination?.route){Scaffold(topBar{CupcakeAppBar(canNavigateBackfalse,navigateUp{})}){innerPadding-valuiStatebyviewModel.uiState.collectAsState()NavHost(navControllernavController,startDestinationCupcakeScreen.Start.name,// 设置导航站显示的起始页modifierModifier.padding(innerPadding)// 也可以接受modifier参数){// 显示首页composable(routeCupcakeScreen.Start.name){StartOrderScreen(quantityOptionsDataSource.quantityOptions,modifierModifier.fillMaxSize().padding(dimensionResource(R.dimen.padding_medium)),onNextButtonClicked{viewModel.setQuantity(it)// 导航控制器的跳转 跳转到下一个页面navController.navigate(CupcakeScreen.Flavor.name)})}//flavorcomposable(routeCupcakeScreen.Flavor.name){valcontextLocalContext.currentSelectOptionScreen(subtotaluiState.price,// 通过context 其实现由 Android 系统提供。// 它允许访问应用专用资源和类以及向上调用应用级别的操作例如启动 activity 等。// 您可以使用此变量从视图模型中的资源 ID 列表中获取字符串以显示口味列表optionsDataSource.flavors.map{id-context.resources.getString(id)},onSelectionChanged{viewModel.setFlavor(it)},onNextButtonClicked{navController.navigate(CupcakeScreen.Pickup.name)},onCancelButtonClicked{},modifierModifier.fillMaxHeight())}// pickupcomposable(routeCupcakeScreen.Pickup.name){SelectOptionScreen(subtotaluiState.price,optionsuiState.pickupOptions,onSelectionChanged{viewModel.setDate(it)},onNextButtonClicked{navController.navigate(CupcakeScreen.Summary.name)},onCancelButtonClicked{},modifierModifier.fillMaxHeight())}// ordercomposable(routeCupcakeScreen.Summary.name){OrderSummaryScreen(orderUiStateuiState,modifierModifier.fillMaxHeight(),onCancelButtonClicked{},onSendButtonClicked{subject:String,summary:String-})}}}}1.2.4 跳转到起始屏幕与系统返回按钮不同Cancel 按钮不会返回上一个屏幕。而是跳转移除返回堆栈中的所有屏幕并返回起始屏幕。您可以通过调用popBackStack()方法来实现此目的popBackStack()方法有两个必需参数route 此字符串表示您希望返回到的目标页面的路线inclusive 这是一个布尔值如果为true还会弹出移除指定路线。如果为falsepopBackStack()将移除起始目标页面之上的所有目标页面但不包含该起始目标页面并仅留下该起始目标页面作为最顶层的屏幕显示给用户。当用户在任何屏幕上点按Cancel按钮时应用会重置视图模型中的状态并调用popBackStack()。首先您将实现一个方法来执行此操作然后为包含Cancel按钮的所有三个屏幕的相应参数传入该方法。/** * 定义取消按钮 */privatefuncancelOrderAndNavigateToStart(viewModel:OrderViewModel,navController:NavHostController){viewModel.resetOrder()navController.popBackStack(CupcakeScreen.Start.name,inclusivefalse)}// 为下面两个页面添加取消按钮点击事件// pickupcomposable(routeCupcakeScreen.Pickup.name){SelectOptionScreen(subtotaluiState.price,optionsuiState.pickupOptions,onSelectionChanged{viewModel.setDate(it)},onNextButtonClicked{navController.navigate(CupcakeScreen.Summary.name)},onCancelButtonClicked{// 返回首页重置订单viewmodelcancelOrderAndNavigateToStart(viewModel,navController)},modifierModifier.fillMaxHeight())}// ordercomposable(routeCupcakeScreen.Summary.name){OrderSummaryScreen(orderUiStateuiState,modifierModifier.fillMaxHeight(),onCancelButtonClicked{// 返回首页重置订单viewmodelcancelOrderAndNavigateToStart(viewModel,navController)},onSendButtonClicked{subject:String,summary:String-})}1.3. 导航到其他应用在应用到最后一个页面我们生成了一个订单 需要把订单的信息发送给其他应用。此选项会打开一个 ShareSheet覆盖屏幕底部部分的界面组件其中会显示分享选项。此部分界面不属于 Cupcake 应用。事实上它是由 Android 操作系统提供的。您的 navController 不会调用系统界面例如分享屏幕。作为替代方案您可以使用 intent。intent将请求系统执行某项操作通常用于呈现新的 activity。有许多不同的intent建议您参阅相关文档查看完整列表。不过我们感兴趣的是ACTION_SEND。您可以向此 intent 提供某些数据例如字符串并为这些数据提供适当的分享操作。设置 intent 的基本过程如下创建一个 intent 对象并指定 intent例如 ACTION_SEND。指定随 intent 一同发送的其他数据类型。对于简单的一段文本您可以使用 “text/plain”但也可以使用其他类型例如 “image/ 或 video/”。通过调用putExtra()方法向intent传递任何其他数据例如要分享的文本或图片。此 intent 将接受两个 extraEXTRA_SUBJECT和EXTRA_TEXT。调用上下文的startActivity()方法并传入从intent创建的activity。privatefunshareOrder(context:Context,subject:String,summary:String){// 创建一个Intent对象// 由于只需要配置一次Intent对象 可以直接使用apply函数 让配置代码更简洁valintentIntent(Intent.ACTION_SEND).apply{// 设置类型typetext/plain// 传递数据putExtra(Intent.EXTRA_SUBJECT,subject)putExtra(Intent.EXTRA_TEXT,summary)}// 跳转另外一个activitycontext.startActivity(Intent.createChooser(intent,context.getString(R.string.new_cupcake_order)))}// 调用方法地方// ordercomposable(routeCupcakeScreen.Summary.name){// 获取当前context 上下文valcontextLocalContext.current;OrderSummaryScreen(orderUiStateuiState,modifierModifier.fillMaxHeight(),onCancelButtonClicked{cancelOrderAndNavigateToStart(viewModel,navController)},onSendButtonClicked{subject:String,summary:String-// 调用分享方法shareOrder(context,subject,summary)})}1.4. 让应用栏响应导航现在我们可以再多个页面之间导航但是页面中还缺少一点东西。当我们导航到一个新的页面 不会更新标题也不会适当的在标题前面显示向上按钮在我们的代码中包含一个CupcakeAppBar的AppBar。OptIn(ExperimentalMaterial3Api::class)ComposablefunCupcakeAppBar(canNavigateBack:Boolean,navigateUp:()-Unit,modifier:ModifierModifier){TopAppBar(title{Text(stringResource(idR.string.app_name))},colorsTopAppBarDefaults.mediumTopAppBarColors(containerColorMaterialTheme.colorScheme.primaryContainer),modifiermodifier,navigationIcon{if(canNavigateBack){IconButton(onClicknavigateUp){Icon(imageVectorIcons.Filled.ArrowBack,contentDescriptionstringResource(R.string.back_button))}}})}在我们的代码中实现了导航栏 现在需要使用堆栈中的信息来正确的显示标题 并适当的显示导航栏中返回按钮。CupcakeAppBar可组合项应了解当前屏幕以便标题进行相应更新。首先我们对导航的枚举做一些改造给枚举添加一个参数 将每个页面的标题和枚举对应起来。enumclassCupcakeScreen(StringResvaltitle:Int){Start(titleR.string.app_name),Flavor(titleR.string.choose_flavor),Pickup(titleR.string.choose_pickup_date),Summary(titleR.string.order_summary)}然后 修改CupcakApp可组合方法ComposablefunCupcakeApp(viewModel:OrderViewModelOrderViewModel(),navController:NavHostControllerrememberNavController()){// 获取当前返回栈条目状态 currentBackStackEntryAsState 将当前返回栈条目转换为 Compose 状态valbackStackEntrybynavController.currentBackStackEntryAsState()// 根据当前返回栈得到当前的页面所对象的枚举对象valcurrentScreenCupcakeScreen.valueOf(// 获取当前目的地页面的路由字符串 如果为空 则使用默认值backStackEntry?.destination?.route?:CupcakeScreen.Start.name)Scaffold(topBar{CupcakeAppBar(currentScreen,canNavigateBacknavController.previousBackStackEntry!null,navigateUp{// 返回上一个页面的操作navController.navigateUp()})}){innerPadding-valuiStatebyviewModel.uiState.collectAsState()NavHost(navControllernavController,startDestinationCupcakeScreen.Start.name,modifierModifier.padding(innerPadding)){/// ....}}}1.5 保存页面状态importandroid.os.Bundleimportandroidx.activity.ComponentActivityimportandroidx.activity.compose.setContentimportandroidx.compose.foundation.layout.*importandroidx.compose.foundation.lazy.LazyColumnimportandroidx.compose.foundation.lazy.itemsimportandroidx.compose.material.icons.Iconsimportandroidx.compose.material.icons.filled.Dashboardimportandroidx.compose.material.icons.filled.Homeimportandroidx.compose.material.icons.filled.Personimportandroidx.compose.material3.*importandroidx.compose.runtime.Composableimportandroidx.compose.runtime.getValueimportandroidx.compose.ui.Modifierimportandroidx.compose.ui.graphics.vector.ImageVectorimportandroidx.compose.ui.unit.dpimportandroidx.navigation.NavGraph.Companion.findStartDestinationimportandroidx.navigation.NavHostControllerimportandroidx.navigation.compose.NavHostimportandroidx.navigation.compose.composableimportandroidx.navigation.compose.currentBackStackEntryAsStateimportandroidx.navigation.compose.rememberNavController// // 1. 定义导航路由和底部 Tab 的数据结构// sealedclassScreen(valroute:String,vallabel:String,valicon:ImageVector){objectHome:Screen(home,首页,Icons.Filled.Home)objectDashboard:Screen(dashboard,仪表盘,Icons.Filled.Dashboard)objectProfile:Screen(profile,我的,Icons.Filled.Person)}// 包含所有底部 Tab 的列表valbottomNavItemslistOf(Screen.Home,Screen.Dashboard,Screen.Profile)// // 2. 主界面 (整合 Scaffold, BottomNav, NavHost)// classMainActivity:ComponentActivity(){overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)setContent{MaterialTheme{MainScreen()}}}}ComposablefunMainScreen(){valnavControllerrememberNavController()// 获取当前的路由状态用于高亮底部 TabvalnavBackStackEntrybynavController.currentBackStackEntryAsState()valcurrentRoutenavBackStackEntry?.destination?.routeScaffold(// 底部导航栏bottomBar{BottomNav(navControllernavController,currentRoutecurrentRoute)}){innerPadding-// 路由容器NavHost(navControllernavController,startDestinationScreen.Home.route,modifierModifier.padding(innerPadding)// 避免被底部导航遮挡){composable(Screen.Home.route){HomeScreen()}composable(Screen.Dashboard.route){DashboardScreen()}composable(Screen.Profile.route){ProfileScreen()}}}}// // 3. 自定义的底部导航组件 (对应你截图中的 BottomNav)// ComposablefunBottomNav(navController:NavHostController,currentRoute:String?){NavigationBar{bottomNavItems.forEach{screen-NavigationBarItem(icon{Icon(screen.icon,contentDescriptionscreen.label)},label{Text(screen.label)},selectedcurrentRoutescreen.route,onClick{// 核心导航逻辑 navController.navigate(screen.route){// 1. 弹出到起始页并保存当前状态popUpTo(navController.graph.findStartDestination().id){saveStatetrue}// 2. 避免重复创建相同页面launchSingleToptrue// 3. 恢复之前保存的状态restoreStatetrue}})}}}// // 4. 各个页面的实现// // 首页带有一个长列表用于测试状态保存ComposablefunHomeScreen(){LazyColumn(modifierModifier.fillMaxSize(),contentPaddingPaddingValues(16.dp),verticalArrangementArrangement.spacedBy(8.dp)){items(50){index-Card(modifierModifier.fillMaxWidth()){Text(text首页列表 Item$index,modifierModifier.padding(24.dp))}}}}// 仪表盘ComposablefunDashboardScreen(){Box(modifierModifier.fillMaxSize(),contentAlignmentandroidx.compose.ui.Alignment.Center){Text(仪表盘页面,styleMaterialTheme.typography.headlineLarge)}}// 我的ComposablefunProfileScreen(){Box(modifierModifier.fillMaxSize(),contentAlignmentandroidx.compose.ui.Alignment.Center){Text(个人中心页面,styleMaterialTheme.typography.headlineLarge)}}二. 测试Cupacke应用参考链接:官方文档地址