【Android】Navigation

发布时间:2026/11/6 4:59:39
【Android】Navigation

一. Navigation

1.1. 导航组件的组成部分

Navigation组件有主要的三个部分:

  • NaviController:负责在目标页面(即应用中的屏幕)之间导航。
  • NavGraph:用于映射要导航到的可组合项目标页面。
  • NavHost:此可组合项充当容器,用于显示NavGraph的当前目标页面。

1.2. 在应用中为目标页面定义路线

在 Compose 应用中,导航的一个基本概念就是路线。路线是与目标页面对应的字符串。这类似于网址的概念。就像不同网址映射到网站上的不同页面一样,路线是可映射至目标页面并作为其唯一标识符的字符串。

应用中的屏幕数量有限,因此路线数量也是有限的。您可以使用枚举类来定义应用的路线。Kotlin 中的枚举类具有一个名称属性,该属性会返回具有属性名称的字符串。

1.2.1 定义枚举

首先我们需要定义一个枚举来定义路线名称:

enumclassCupcakeScreen(){Start,Flavor,Pickup,Summary}

1.2.2. 添加NavHost

NavHost 是一个可组合项,用于根据给定路线来显示其他可组合项目标页面。例如,如果路线为 Flavor,NavHost会显示用于选择纸杯蛋糕口味的屏幕。如果路线为 Summary,则应用会显示摘要屏幕。

NavHost的语法和其他可组合项的语法一样:

这里有两个参数是需要注意:

  • navController:NavHostController类的实例。您可以使用此对象在屏幕之间导航。例如,通过调用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:OrderViewModel=OrderViewModel(),// 管理数据的viewModel对象navController:NavHostController=rememberNavController()// NavController的实例// 监听当前页面变化,页面切换时自动更新valnavBackStackEntrybynavController.currentBackStackEntryAsState()// 当前页面的路由名称valcurrentRoute=navBackStackEntry.destination?.route){Scaffold(topBar={CupcakeAppBar(canNavigateBack=false,navigateUp={})}){innerPadding->valuiStatebyviewModel.uiState.collectAsState()NavHost(navController=navController,startDestination=CupcakeScreen.Start.name,// 设置导航站显示的起始页modifier=Modifier.padding(innerPadding)// 也可以接受modifier参数){// 显示首页composable(route=CupcakeScreen.Start.name){StartOrderScreen(quantityOptions=DataSource.quantityOptions,modifier=Modifier.fillMaxSize().padding(dimensionResource(R.dimen.padding_medium)),onNextButtonClicked={viewModel.setQuantity(it)// 导航控制器的跳转, 跳转到下一个页面navController.navigate(CupcakeScreen.Flavor.name)})}//flavorcomposable(route=CupcakeScreen.Flavor.name){valcontext=LocalContext.currentSelectOptionScreen(subtotal=uiState.price,// 通过context 其实现由 Android 系统提供。// 它允许访问应用专用资源和类,以及向上调用应用级别的操作(例如启动 activity 等)。// 您可以使用此变量从视图模型中的资源 ID 列表中获取字符串以显示口味列表options=DataSource.flavors.map{id->context.resources.getString(id)},onSelectionChanged={viewModel.setFlavor(it)},onNextButtonClicked={navController.navigate(CupcakeScreen.Pickup.name)},onCancelButtonClicked={},modifier=Modifier.fillMaxHeight())}// pickupcomposable(route=CupcakeScreen.Pickup.name){SelectOptionScreen(subtotal=uiState.price,options=uiState.pickupOptions,onSelectionChanged={viewModel.setDate(it)},onNextButtonClicked={navController.navigate(CupcakeScreen.Summary.name)},onCancelButtonClicked={},modifier=Modifier.fillMaxHeight())}// ordercomposable(route=CupcakeScreen.Summary.name){OrderSummaryScreen(orderUiState=uiState,modifier=Modifier.fillMaxHeight(),onCancelButtonClicked={},onSendButtonClicked={subject:String,summary:String->})}}}}

1.2.4 跳转到起始屏幕

与系统返回按钮不同,Cancel 按钮不会返回上一个屏幕。而是跳转移除返回堆栈中的所有屏幕,并返回起始屏幕。

您可以通过调用popBackStack()方法来实现此目的

popBackStack()方法有两个必需参数:

  • route: 此字符串表示您希望返回到的目标页面的路线
  • inclusive: 这是一个布尔值,如果为true,还会弹出(移除)指定路线。如果为false,popBackStack()将移除起始目标页面之上的所有目标页面,但不包含该起始目标页面,并仅留下该起始目标页面作为最顶层的屏幕显示给用户。

当用户在任何屏幕上点按Cancel按钮时,应用会重置视图模型中的状态并调用popBackStack()。首先,您将实现一个方法来执行此操作,然后为包含Cancel按钮的所有三个屏幕的相应参数传入该方法。

/** * 定义取消按钮 */privatefuncancelOrderAndNavigateToStart(viewModel:OrderViewModel,navController:NavHostController){viewModel.resetOrder()navController.popBackStack(CupcakeScreen.Start.name,inclusive=false)}// 为下面两个页面添加取消按钮点击事件// pickupcomposable(route=CupcakeScreen.Pickup.name){SelectOptionScreen(subtotal=uiState.price,options=uiState.pickupOptions,onSelectionChanged={viewModel.setDate(it)},onNextButtonClicked={navController.navigate(CupcakeScreen.Summary.name)},onCancelButtonClicked={// 返回首页,重置订单viewmodelcancelOrderAndNavigateToStart(viewModel,navController)},modifier=Modifier.fillMaxHeight())}// ordercomposable(route=CupcakeScreen.Summary.name){OrderSummaryScreen(orderUiState=uiState,modifier=Modifier.fillMaxHeight(),onCancelButtonClicked={// 返回首页,重置订单viewmodelcancelOrderAndNavigateToStart(viewModel,navController)},onSendButtonClicked={subject:String,summary:String->})}

1.3. 导航到其他应用

在应用到最后一个页面,我们生成了一个订单, 需要把订单的信息发送给其他应用。此选项会打开一个 ShareSheet(覆盖屏幕底部部分的界面组件),其中会显示分享选项。

此部分界面不属于 Cupcake 应用。事实上,它是由 Android 操作系统提供的。您的 navController 不会调用系统界面(例如分享屏幕)。作为替代方案,您可以使用 intent。

intent将请求系统执行某项操作,通常用于呈现新的 activity。有许多不同的intent,建议您参阅相关文档,查看完整列表。不过,我们感兴趣的是ACTION_SEND。您可以向此 intent 提供某些数据(例如字符串),并为这些数据提供适当的分享操作。

设置 intent 的基本过程如下:

  1. 创建一个 intent 对象并指定 intent,例如 ACTION_SEND。
  2. 指定随 intent 一同发送的其他数据类型。对于简单的一段文本,您可以使用 “text/plain”,但也可以使用其他类型,例如 “image/" 或 "video/”。
  3. 通过调用putExtra()方法,向intent传递任何其他数据,例如要分享的文本或图片。此 intent 将接受两个 extra:EXTRA_SUBJECT和EXTRA_TEXT。
  4. 调用上下文的startActivity()方法,并传入从intent创建的activity。
privatefunshareOrder(context:Context,subject:String,summary:String){// 创建一个Intent对象// 由于只需要配置一次Intent对象, 可以直接使用apply函数, 让配置代码更简洁valintent=Intent(Intent.ACTION_SEND).apply{// 设置类型type="text/plain"// 传递数据putExtra(Intent.EXTRA_SUBJECT,subject)putExtra(Intent.EXTRA_TEXT,summary)}// 跳转另外一个activitycontext.startActivity(Intent.createChooser(intent,context.getString(R.string.new_cupcake_order)))}// 调用方法地方:// ordercomposable(route=CupcakeScreen.Summary.name){// 获取当前context 上下文valcontext=LocalContext.current;OrderSummaryScreen(orderUiState=uiState,modifier=Modifier.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:Modifier=Modifier){TopAppBar(title={Text(stringResource(id=R.string.app_name))},colors=TopAppBarDefaults.mediumTopAppBarColors(containerColor=MaterialTheme.colorScheme.primaryContainer),modifier=modifier,navigationIcon={if(canNavigateBack){IconButton(onClick=navigateUp){Icon(imageVector=Icons.Filled.ArrowBack,contentDescription=stringResource(R.string.back_button))}}})}

在我们的代码中实现了导航栏, 现在需要使用堆栈中的信息来正确的显示标题, 并适当的显示导航栏中返回按钮。

CupcakeAppBar可组合项应了解当前屏幕,以便标题进行相应更新。

首先我们对导航的枚举做一些改造,给枚举添加一个参数, 将每个页面的标题和枚举对应起来。

enumclassCupcakeScreen(@StringResvaltitle:Int){Start(title=R.string.app_name),Flavor(title=R.string.choose_flavor),Pickup(title=R.string.choose_pickup_date),Summary(title=R.string.order_summary)}

然后 修改CupcakApp()可组合方法:

@ComposablefunCupcakeApp(viewModel:OrderViewModel=OrderViewModel(),navController:NavHostController=rememberNavController()){// 获取当前返回栈条目状态 currentBackStackEntryAsState 将当前返回栈条目转换为 Compose 状态valbackStackEntrybynavController.currentBackStackEntryAsState()// 根据当前返回栈,得到当前的页面所对象的枚举对象valcurrentScreen=CupcakeScreen.valueOf(// 获取当前目的地(页面)的路由字符串, 如果为空, 则使用默认值backStackEntry?.destination?.route?:CupcakeScreen.Start.name)Scaffold(topBar={CupcakeAppBar(currentScreen,canNavigateBack=navController.previousBackStackEntry!=null,navigateUp={// 返回上一个页面的操作navController.navigateUp()})}){innerPadding->valuiStatebyviewModel.uiState.collectAsState()NavHost(navController=navController,startDestination=CupcakeScreen.Start.name,modifier=Modifier.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 的列表valbottomNavItems=listOf(Screen.Home,Screen.Dashboard,Screen.Profile)// ==========================================// 2. 主界面 (整合 Scaffold, BottomNav, NavHost)// ==========================================classMainActivity:ComponentActivity(){overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)setContent{MaterialTheme{MainScreen()}}}}@ComposablefunMainScreen(){valnavController=rememberNavController()// 获取当前的路由状态,用于高亮底部 TabvalnavBackStackEntrybynavController.currentBackStackEntryAsState()valcurrentRoute=navBackStackEntry?.destination?.routeScaffold(// 底部导航栏bottomBar={BottomNav(navController=navController,currentRoute=currentRoute)}){innerPadding->// 路由容器NavHost(navController=navController,startDestination=Screen.Home.route,modifier=Modifier.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,contentDescription=screen.label)},label={Text(screen.label)},selected=currentRoute==screen.route,onClick={// ============ 核心导航逻辑 ============navController.navigate(screen.route){// 1. 弹出到起始页,并保存当前状态popUpTo(navController.graph.findStartDestination().id){saveState=true}// 2. 避免重复创建相同页面launchSingleTop=true// 3. 恢复之前保存的状态restoreState=true}})}}}// ==========================================// 4. 各个页面的实现// ==========================================// 首页:带有一个长列表,用于测试状态保存@ComposablefunHomeScreen(){LazyColumn(modifier=Modifier.fillMaxSize(),contentPadding=PaddingValues(16.dp),verticalArrangement=Arrangement.spacedBy(8.dp)){items(50){index->Card(modifier=Modifier.fillMaxWidth()){Text(text="首页列表 Item$index",modifier=Modifier.padding(24.dp))}}}}// 仪表盘@ComposablefunDashboardScreen(){Box(modifier=Modifier.fillMaxSize(),contentAlignment=androidx.compose.ui.Alignment.Center){Text("仪表盘页面",style=MaterialTheme.typography.headlineLarge)}}// 我的@ComposablefunProfileScreen(){Box(modifier=Modifier.fillMaxSize(),contentAlignment=androidx.compose.ui.Alignment.Center){Text("个人中心页面",style=MaterialTheme.typography.headlineLarge)}}

二. 测试Cupacke应用

参考链接:官方文档地址