这一节主要了解一下Compose中的NavController,它是实现导航功能的核心组件,提供了强大而灵活的页面管理能力,用于管理导航图中的目的地和执行导航操作。
API
navigate(route: String)
含义:导航到指定路由的目的地。
作用:触发页面跳转,可以携带参数(如navigate("detail/{id}"))。
popBackStack()
含义:返回上一个目的地(从返回栈弹出当前页面)。
作用:模拟返回按钮行为,可指定是否弹出到特定路由。
popBackStack(route: String, inclusive: Boolean)
含义:弹出返回栈直到指定路由。
参数:
route:目标路由。
inclusive:是否包含目标路由(true则目标路由也会被弹出)。
currentBackStackEntry
含义:当前显示的目的地的NavBackStackEntry。
作用:获取当前页面的状态、参数或传递数据。
previousBackStackEntry
含义:返回栈中前一个目的地的NavBackStackEntry。
作用:与前一个页面通信(如返回数据)。
优点:
1. 声明式导航
与 Compose 风格一致:基于状态驱动,通过修改返回栈状态而非命令式调用实现导航,更符合Compose的设计哲学。
简化导航逻辑:无需手动管理 Activity/Fragment生命周期,导航状态自动保存与恢复。
2. 灵活的参数传递
支持多种参数类型:可传递基本类型、Parcelable对象,甚至通过savedStateHandle传递任意复杂数据。
深层链接友好:通过路由模板和NavType轻松实现带参数的外部链接导航。
缺点:
1复杂场景支持有限
动态路由困难:运行时动态添加或修改路由较复杂,需手动管理导航图。
跨模块导航繁琐:在模块化项目中,导航图的拆分和集成需要额外配置。
栗子:
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@Composable
fun NavigationExample() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home") {
composable("home") {
HomeScreen(navController = navController)
}
composable("detail") {
DetailScreen(navController = navController)
}
}
}
@Composable
fun HomeScreen(navController: NavHostController) {
Button(onClick = { navController.navigate("detail") }) {
Text("前往详情页")
}
}
@Composable
fun DetailScreen(navController: NavHostController) {
Button(onClick = { navController.popBackStack() }) {
Text("返回首页")
}
}
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.*
import androidx.navigation.compose.*
import androidx.compose.material.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@Composable
fun NavigationExampleTest() {
val navController = rememberNavController()
val authViewModel: AuthViewModel = viewModel()
val isAuthenticated by authViewModel.isAuthenticated.collectAsState()
NavHost(navController = navController, startDestination = "splash") {
// 1. 闪屏页(检查登录状态)
composable("splash") {
SplashScreen(navController = navController)
}
// 2. 登录/注册流程
composable("login") {
LoginScreen(
navController = navController,
onLoginSuccess = { authViewModel.login() }
)
}
navigation(route = "app", startDestination = "home") {
composable("home") {
HomeScreen2(navController = navController)
}
composable("cart") {
if (isAuthenticated) {
CartScreen2(navController = navController)
} else {
navController.currentBackStackEntry?.savedStateHandle?.set("targetRoute", "cart")
navController.navigate("login")
}
}
composable("profile") {
if (isAuthenticated) {
// ProfileScreen(navController = navController)
} else {
navController.currentBackStackEntry?.savedStateHandle?.set("targetRoute", "profile")
navController.navigate("login")
}
}
composable(
route = "product/{productId}",
arguments = listOf(navArgument("productId") { type = NavType.IntType }),
deepLinks = listOf(
navDeepLink { uriPattern = "https://example.com/products/{productId}" },
navDeepLink { uriPattern = "myapp://products/{productId}" }
)
) { backStackEntry ->
val productId = backStackEntry.arguments?.getInt("productId") ?: 0
ProductDetailScreen(
productId = productId,
navController = navController
)
}
}
}
}
@Composable
fun SplashScreen(navController: NavHostController) {
val context = LocalContext.current
LaunchedEffect(Unit) {
delay(1500)
val isLoggedIn = checkLoginStatus(context)
if (isLoggedIn) {
navController.navigate("app") {
popUpTo("splash") { inclusive = true }
}
} else {
navController.navigate("login") {
popUpTo("splash") { inclusive = true }
}
}
}
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
@Composable
fun LoginScreen(
navController: NavHostController,
onLoginSuccess: () -> Unit
) {
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
TextField(
value = username,
onValueChange = { username = it },
label = { Text("用户名") }
)
TextField(
value = password,
onValueChange = { password = it },
label = { Text("密码") },
visualTransformation = PasswordVisualTransformation()
)
Button(
onClick = {
onLoginSuccess()
val targetRoute = navController.previousBackStackEntry?.savedStateHandle?.get<String>("targetRoute")
if (targetRoute != null) {
navController.navigate("app/$targetRoute") {
popUpTo("login") { inclusive = true }
}
} else {
navController.navigate("app") {
popUpTo("login") { inclusive = true }
}
}
}
) {
Text("登录")
}
}
}
@Composable
fun HomeScreen2(navController: NavHostController) {
Button(onClick = { navController.navigate("product/123") }) {
Text("查看热门商品")
}
}
@Composable
fun ProductDetailScreen(
productId: Int,
navController: NavHostController
) {
val product = remember { Product2(productId, "商品$productId", 99.99) }
Column {
Text(product.name, style = MaterialTheme.typography.h4)
Text("¥${product.price}", style = MaterialTheme.typography.h5)
}
}
@Composable
fun CartScreen2(navController: NavHostController) {
val cartViewModel: CartViewModel = viewModel()
val cartItems by cartViewModel.cartItems.collectAsState()
Column {
Text("购物车 (${cartItems.size})")
Button(onClick = {
// 创建订单并导航到结算页
val orderId = createOrder(cartItems)
navController.navigate("app/checkout/$orderId")
}) {
Text("结算")
}
}
}
data class Product2(val id: Int, val name: String, val price: Double)
data class CartItem(val product: Product, val quantity: Int)
private fun checkLoginStatus(context: android.content.Context): Boolean {
return false
}
private fun createOrder(cartItems: List<CartItem>): String {
return System.currentTimeMillis().toString()
}
class AuthViewModel : ViewModel() {
private val _isAuthenticated = MutableStateFlow(false)
val isAuthenticated: StateFlow<Boolean> = _isAuthenticated
fun login() {
_isAuthenticated.value = true
}
fun logout() {
_isAuthenticated.value = false
}
}
class CartViewModel : ViewModel() {
private val _cartItems = MutableStateFlow(emptyList<CartItem>())
val cartItems: StateFlow<List<CartItem>> = _cartItems
fun addToCart(product: Product, quantity: Int = 1) {
// 更新购物车逻辑...
}
}
注意:
1 避免深层嵌套
过度嵌套(如三层以上导航)会导致返回栈管理复杂,建议通过参数传递替代深层导航。
示例:用单个详情页接收不同参数,替代多个层级的详情子页。
2 合理拆分导航图
在模块化项目中,按功能拆分导航图(如用户模块、商品模块),避免单个导航图过大。
使用androidx.navigation:navigation-dynamic-features支持动态加载导航图。
3 明确路由命名
使用语义化路由(如product/{id}而非screen3),提高代码可读性。
避免路由参数与路径混淆(如product?id=123 vs product/123)。
4 避免大对象传递
路由参数(如arguments)应尽量传递 ID 而非完整对象,防止内存占用过高。
示例:传递商品 ID 而非整个Product对象,在目标页面通过 ID 加载数据。
5 合理使用savedStateHandle
仅用于短期数据传递(如页面返回结果),避免存储大量或长期数据。
及时清理不再需要的数据,防止内存泄漏。
源码:
1. 导航操作入口
navigate函数是触发导航的主要入口,它接收一个路由字符串作为参数,用于指定要导航到的目标屏幕。
public fun navigate(route: String, builder: NavOptionsBuilder.() -> Unit = {}) {
val navOptions = NavOptionsBuilder().apply(builder).build()
navigate(route, navOptions)
}
这里首先通过NavOptionsBuilder构建导航选项,然后调用另一个navigate重载方法。
2. 路由解析与匹配
在navigate方法内部,会通过Navigator来解析路由并找到对应的目的地。
val navigator = navigatorProvider.getNavigator(route)
val destination = navigator.findDestination(route)
navigatorProvider是一个用于获取不同类型Navigator的提供者,它根据路由的类型(例如,是否是深层链接等)来获取相应的Navigator。然后,通过Navigator的findDestination方法在导航图中查找与路由匹配的Destination。
3. 处理导航逻辑
找到目标Destination后,会根据导航选项(如是否添加到返回栈、是否启动新任务等)来执行具体的导航逻辑。
if (navOptions.shouldLaunchSingleTop && currentBackStackEntry?.destination == destination) {
// 处理单顶模式,即如果目标屏幕已经在栈顶,执行相应的回调
onDestinationReached(destination)
return
}
val newBackStackEntry = createBackStackEntry(destination, navOptions)
if (navOptions.addToBackStack) {
// 如果需要添加到返回栈,将新的BackStackEntry添加到返回栈中
backStack.add(newBackStackEntry)
}
// 执行导航,更新当前显示的屏幕
setCurrentBackStackEntry(newBackStackEntry)
这里首先处理了单顶模式的情况,如果目标屏幕已经在栈顶,会执行相应的回调。然后创建一个新的BackStackEntry,它包含了目标屏幕的相关信息。如果导航选项要求添加到返回栈,就将新的BackStackEntry添加到返回栈中。最后,通过setCurrentBackStackEntry方法更新当前显示的屏幕,这会触发Composable的重新组合,以显示新的屏幕。
4. 返回操作逻辑
当执行返回操作时,例如通过系统返回键或者调用popBackStack方法,会从返回栈中弹出当前的BackStackEntry。
public fun popBackStack(): Boolean {
val currentEntry = currentBackStackEntry ?: return false
val newIndex = backStack.indexOf(currentEntry) - 1
if (newIndex < 0) {
// 已经在栈底,无法返回
return false
}
// 弹出当前BackStackEntry
backStack.removeAt(newIndex + 1)
// 设置新的当前BackStackEntry
setCurrentBackStackEntry(backStack[newIndex])
return true
}
首先获取当前的BackStackEntry,然后计算返回后的索引位置。如果索引小于 0,表示已经在栈底,无法返回。否则,从返回栈中移除当前的BackStackEntry,并设置新的当前BackStackEntry,从而实现返回上一个屏幕的功能。
简单总结,NavController通过这种基于返回栈的状态管理和路由匹配机制,实现了Compose中灵活且高效的导航功能,使得开发者能够方便地管理应用中的屏幕导航和状态切换。