Skip to content

Compose 导航

欢迎来到 Compose 导航的世界!导航是移动应用中非常重要的一部分,它决定了用户如何在不同的屏幕之间切换。Compose 提供了一个名为 Jetpack Navigation Compose 的库,用于处理应用内的导航,让我们一起探索吧!

🎯 什么是导航?

导航是指用户在应用中从一个屏幕(或目的地)移动到另一个屏幕的过程。在 Compose 中,导航通常用于:

  • 在不同的功能模块之间切换
  • 展示详细信息
  • 处理用户流程(如注册、登录)
  • 等等...

🧩 集成导航库

首先,你需要在项目中添加 Navigation Compose 的依赖:

kotlin
dependencies {
    implementation("androidx.navigation:navigation-compose:2.5.3")
}

🧩 设置导航

创建 NavController

NavController 是导航的核心,用于管理应用的导航状态:

kotlin
@Composable
fun MyApp() {
    val navController = rememberNavController()
    // ...
}

创建导航图

导航图定义了应用中的所有目的地和它们之间的连接:

kotlin
@Composable
fun MyApp() {
    val navController = rememberNavController()
    
    NavHost(navController = navController, startDestination = "home") {
        composable(route = "home") {
            HomeScreen(navController = navController)
        }
        composable(route = "detail") {
            DetailScreen(navController = navController)
        }
    }
}

🧩 导航操作

导航到新目的地

使用 navigate 方法导航到新目的地:

kotlin
@Composable
fun HomeScreen(navController: NavController) {
    Button(onClick = { navController.navigate("detail") }) {
        Text(text = "Go to Detail")
    }
}

返回上一目的地

使用 popBackStack 方法返回上一目的地:

kotlin
@Composable
fun DetailScreen(navController: NavController) {
    Button(onClick = { navController.popBackStack() }) {
        Text(text = "Go back")
    }
}

替换当前目的地

使用 navigate 方法并设置 launchSingleTop = true 来替换当前目的地:

kotlin
@Composable
fun HomeScreen(navController: NavController) {
    Button(onClick = {
        navController.navigate("detail") {
            launchSingleTop = true
        }
    }) {
        Text(text = "Go to Detail")
    }
}

🧩 传递参数

定义带参数的路由

你可以在路由中定义参数:

kotlin
NavHost(navController = navController, startDestination = "home") {
    composable(route = "home") {
        HomeScreen(navController = navController)
    }
    composable(route = "detail/{itemId}") {
        backStackEntry ->
        val itemId = backStackEntry.arguments?.getString("itemId")
        DetailScreen(itemId = itemId, navController = navController)
    }
}

传递参数

使用参数导航到目的地:

kotlin
@Composable
fun HomeScreen(navController: NavController) {
    Button(onClick = {
        navController.navigate("detail/123")
    }) {
        Text(text = "Go to Detail with ID 123")
    }
}

可选参数

你可以使用查询参数的形式传递可选参数:

kotlin
NavHost(navController = navController, startDestination = "home") {
    composable(
        route = "detail?id={itemId}&name={itemName}",
        arguments = listOf(
            navArgument("itemId") {
                type = NavType.IntType
                defaultValue = 0
            },
            navArgument("itemName") {
                type = NavType.StringType
                defaultValue = ""
            }
        )
    ) {
        backStackEntry ->
        val itemId = backStackEntry.arguments?.getInt("itemId")
        val itemName = backStackEntry.arguments?.getString("itemName")
        DetailScreen(itemId = itemId, itemName = itemName, navController = navController)
    }
}
kotlin
@Composable
fun HomeScreen(navController: NavController) {
    Button(onClick = {
        navController.navigate("detail?id=123&name=Item%201")
    }) {
        Text(text = "Go to Detail with params")
    }
}

🧩 深层链接

深层链接允许用户从应用外部直接导航到应用内部的特定目的地:

定义深层链接

kotlin
NavHost(navController = navController, startDestination = "home") {
    composable(
        route = "detail/{itemId}",
        deepLinks = listOf(
            navDeepLink {
                uriPattern = "https://myapp.com/detail/{itemId}"
            }
        )
    ) {
        backStackEntry ->
        val itemId = backStackEntry.arguments?.getString("itemId")
        DetailScreen(itemId = itemId, navController = navController)
    }
}

在 AndroidManifest.xml 中注册深层链接

xml
<activity
    android:name=".MainActivity"
    android:exported="true"
    android:theme="@style/Theme.MyApp">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data
            android:scheme="https"
            android:host="myapp.com"
            android:pathPrefix="/detail" />
    </intent-filter>
</activity>

🧩 导航过渡动画

你可以为导航添加过渡动画:

kotlin
NavHost(navController = navController, startDestination = "home") {
    composable(
        route = "home",
        enterTransition = { slideInHorizontally(initialOffsetX = { -it }) },
        exitTransition = { slideOutHorizontally(targetOffsetX = { -it }) },
        popEnterTransition = { slideInHorizontally(initialOffsetX = { it }) },
        popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) }
    ) {
        HomeScreen(navController = navController)
    }
    // ...
}

🎯 导航的最佳实践

1. 避免传递复杂对象

导航参数应该是简单的类型(如字符串、整数等),避免传递复杂对象:

kotlin
// 不好的做法:传递复杂对象
@Composable
fun HomeScreen(navController: NavController) {
    val item = Item(id = 1, name = "Item 1")
    Button(onClick = {
        navController.currentBackStackEntry?.arguments?.putParcelable("item", item)
        navController.navigate("detail")
    }) {
        Text(text = "Go to Detail")
    }
}

// 好的做法:传递简单参数
@Composable
fun HomeScreen(navController: NavController) {
    Button(onClick = {
        navController.navigate("detail/1")
    }) {
        Text(text = "Go to Detail")
    }
}

// 在目的地中获取对象
@Composable
fun DetailScreen(navController: NavController) {
    val itemId = navController.currentBackStackEntry?.arguments?.getString("itemId")
    val item = repository.getItemById(itemId?.toInt() ?: 0)
    // ...
}

2. 使用 ViewModel 管理导航数据

你可以使用 ViewModel 来管理导航数据,避免在目的地之间直接传递数据:

kotlin
class SharedViewModel : ViewModel() {
    private val _selectedItem = MutableLiveData<Item>()
    val selectedItem: LiveData<Item> = _selectedItem
    
    fun selectItem(item: Item) {
        _selectedItem.value = item
    }
}

@Composable
fun HomeScreen(navController: NavController, sharedViewModel: SharedViewModel) {
    val item = Item(id = 1, name = "Item 1")
    Button(onClick = {
        sharedViewModel.selectItem(item)
        navController.navigate("detail")
    }) {
        Text(text = "Go to Detail")
    }
}

@Composable
fun DetailScreen(sharedViewModel: SharedViewModel) {
    val item by sharedViewModel.selectedItem.observeAsState()
    // ...
}

3. 保持导航图的简洁

导航图应该保持简洁,避免包含太多的逻辑:

kotlin
// 不好的做法:导航图包含太多逻辑
NavHost(navController = navController, startDestination = "home") {
    composable(route = "home") {
        val viewModel = viewModel<HomeViewModel>()
        val items by viewModel.items.observeAsState()
        HomeScreen(items = items, navController = navController)
    }
    // ...
}

// 好的做法:导航图只负责导航
NavHost(navController = navController, startDestination = "home") {
    composable(route = "home") {
        HomeScreen(navController = navController)
    }
    // ...
}

@Composable
fun HomeScreen(navController: NavController) {
    val viewModel: HomeViewModel = viewModel()
    val items by viewModel.items.observeAsState()
    // ...
}

🎨 练习

现在,让我们来练习一下:创建一个包含两个屏幕的简单导航应用。

kotlin
@Composable
fun MyNavigationApp() {
    val navController = rememberNavController()
    
    NavHost(navController = navController, startDestination = "list") {
        composable(route = "list") {
            ListScreen(navController = navController)
        }
        composable(route = "detail/{itemId}") {
            backStackEntry ->
            val itemId = backStackEntry.arguments?.getString("itemId")?.toInt() ?: 0
            DetailScreen(itemId = itemId, navController = navController)
        }
    }
}

@Composable
fun ListScreen(navController: NavController) {
    val items = listOf(
        Item(1, "Item 1"),
        Item(2, "Item 2"),
        Item(3, "Item 3")
    )
    
    Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
        Text(text = "Item List", style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold))
        LazyColumn(modifier = Modifier.fillMaxSize()) {
            items(items) {
                item ->
                Text(
                    text = item.name,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(16.dp)
                        .clickable { navController.navigate("detail/${item.id}") }
                )
                Divider()
            }
        }
    }
}

@Composable
fun DetailScreen(itemId: Int, navController: NavController) {
    val item = remember { 
        when (itemId) {
            1 -> Item(1, "Item 1", "This is the detail of Item 1")
            2 -> Item(2, "Item 2", "This is the detail of Item 2")
            3 -> Item(3, "Item 3", "This is the detail of Item 3")
            else -> Item(0, "Unknown", "Unknown item")
        }
    }
    
    Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
        Text(text = item.name, style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold))
        Text(text = item.description, modifier = Modifier.padding(vertical = 16.dp))
        Button(onClick = { navController.popBackStack() }) {
            Text(text = "Go back")
        }
    }
}

data class Item(val id: Int, val name: String, val description: String = "")

🎉 恭喜

你已经学习了 Compose 中的导航功能!从基本的导航设置到参数传递和深层链接,这些知识将帮助你创建出具有良好导航体验的应用。

下一节,我们将学习 Compose 中的主题和样式,了解如何自定义应用的外观。

🚀 继续前进吧!