Compose中的导航

127 阅读5分钟

在Android布局还在使用XML时代,我们基于Intent来实现页面的跳转;在Compose时代,我们通过什么来实现页面的跳转呢?本文主要介绍以下几点内容。

1. Navigation是什么

2. A界面如何跳转到B界面

3. A界面如何携带参数跳转到B界面

4. 底部导航栏如何实现跳转

前置条件: 1、引入navigation(可跳过)

navigation-compose = "2.5.3"   //libs.version.tomal 文件

androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" } //libs.version.tomal 文件


implementation(libs.androidx.navigation.compose) //app 目录下build.gradle.kts

2、创建简单的页面:

@Composable
fun  PageA() {
    Text("page A")
}

@Composable
fun  PageB() {
    Text("page B")
}


@Composable
fun  PageC() {
    Text("page C")
}


@Composable
fun  PageD() {
    Text("page D")
}


1752548480001.png

一、 Navigation是什么?

1、Navigation 最早出现在Jetpack 库中,它主要管理应用内导航,提供一种声明式的方式来定义导航路径,简化了组件之间的切换过程;在Compose中Navigation库同样适用,并专门为Compose设计并扩展。

2、4个核心概念(这可比Intent多多了)

2.1、NavController 导航的核心控制器,用于管理导航栈以及执行导航操作。

2.2、NavHost一个容器,用于显示导航图中的目的地。

2.3、Destination(目的地)导航图中的节点,(如 Composable 函数)或模块。

2.4、NavGraph(导航图)一个包含所有目的地和它们之间导航路径(动作)的资源文件。在 Compose 中,我们通常使用 Kotlin DSL 来构建导航图。

名词的解释和理解,总是晦涩难懂的,下面通过一个例子来解释上面的名词;柳如烟从成都出发自驾到重庆,柳如烟就是NavController她负责管理导航的堆栈信息和执行导航操作;她驾驶的玛莎拉蒂就是NavHost(一个容器,用于显示导航图中的目的地);Destination(目的地)导航图中的节点,就是如烟要去的重庆;NavGraph(导航图 如composable)就是如烟使用的xx导航地图。(ps:最近迷上短剧,重生之我在敲代码)

1752550540204.png

二、A界面如何跳转到B界面

2.1 简单修改A界面代码

@Composable
fun PageA() {
    Column(modifier = Modifier.padding(10.dp)) {
        Button(onClick = {

        }) {
           Text("跳转到B界面")
        }
    }
}

2.2创建NavHostController

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
           val navHostController = rememberNavController()
            AppScreen(navHostController)
        }
    }
}


@Composable
fun AppScreen(navHostController:NavHostController) {

}

2.3创建NavHost和NavGraph

@Composable
fun NavGraph(navController: NavHostController) {
    NavHost(
        navController = navController,
        startDestination = Route.PAGE_A
    ) {
        composable(Route.PAGE_A) {
            PageA()
        }

        composable(Route.PAGE_B) {
            PageA()
        }
        composable(Route.PAGE_C) {
            PageB()
        }
    }

}

2.4修改MainActivity AppScreen

@Composable
fun AppScreen(navHostController:NavHostController) {
   NavGraph(navHostController)
}

Screenshot_2025-07-17-13-48-40-742_com.hhx.navigation-edit.jpg

解释一下上面的代码,首先MainActivity 通过setContent 关联到了AppScreen,AppScreen内部调用了NavGraph,NavGrap内部实现了一个NavHost(如烟的玛莎拉蒂)通过导航图(composeable )关联到了其它界面,startDestination就是起始地,默认显示哪个; 也就是说MainActivity默认显示PageA的内容。

2.5 从界面A跳转到界面B

完成导航的具体动作是通过NavController 来执行的,也就是如烟本人想去哪里,由她自己决定。

2.5.1 修改PageA 和NavGraph中的代码来实现跳转

@Composable
fun PageA(onButtonClick:()->Unit) {

    Column(modifier = Modifier.padding(10.dp)) {
        Button(onClick = {
           onButtonClick.invoke()
        }) {
           Text("跳转到B界面")
        }
    }
}
composable(Route.PAGE_A) {
    PageA{
        navController.navigate(Route.PAGE_B)
    }
}

以上就实现了通过NavController来实现最基本的跳转。

三、A界面如何携带参数跳转到B界面

3.1夸界面的参数传递

和Intent一样,一个发送方,一个接收方;发送方通过路径+/$来站位,接收方通过路径+/{}来站位,此外接收方还需要通过backStackEntry.arguments?.getXXX接收参数;

如果有多个参数,发送方需要有多个 /来拼接例/ 来拼接例 /param1/$param2,(这里显示有问题,具体参见代码)同理接收方也需要有多个/{}来占位例如/{param1}/{param2}",并且需要有多个backStackEntry.arguments?.getXXX来接收参数()

3.2修改PageA 和NavGraph中的代码来实现携带参数的跳转

@Composable
fun PageA(onButtonClick:()->Unit,enterPageC:(String,String)->Unit) {

    Column(modifier = Modifier.padding(10.dp)) {
        Button(onClick = {
           onButtonClick.invoke()
        }) {
           Text("跳转到B界面")
        }

        Button(onClick = {
            enterPageC.invoke("Hello","Android")
        }) {
            Text("跳转到C界面")
        }
    }
}
@Composable
fun NavGraph(navController: NavHostController) {
    NavHost(
        navController = navController,
        startDestination = Route.PAGE_A
    ) {
        composable(Route.PAGE_A) {
            PageA(onButtonClick = {
                navController.navigate(Route.PAGE_B)
            }, enterPageC = {param1,param2->
                navController.navigate(Route.PAGE_C+"/$param1/$param2")
            })
        }
        
        composable(
            Route.PAGE_C + "/{param1}/{param2}",
        ) { backStackEntry ->
            val param1 = backStackEntry.arguments?.getString("param1")
            val param2 = backStackEntry.arguments?.getString("param2")
            PageC(param1,param2)
        }

        composable(Route.PAGE_B) {
            PageB()
        }
    }

}

运行效果如下:

Screenshot_2025-07-17-14-53-24-348_com.hhx.navigation.jpg

3.3 有没有觉得上面的路径拼接很麻烦并且还要很小心,如果拼接错误,就不能传递参数了,有没有解决方法呢,当然有我们新建一个NavController的扩展方法

fun NavController.navigate(route:String,args: Bundle?){
    graph.findNode(route)?.let {
        navigate(it.id,args)
    }
}

然后再修改 NavGraph 中的代码

@Composable
fun NavGraph(navController: NavHostController) {
    NavHost(
        navController = navController,
        startDestination = Route.PAGE_A
    ) {
        composable(Route.PAGE_A) {
            PageA(onButtonClick = {
                navController.navigate(Route.PAGE_B)
            }, enterPageC = {param1,param2->
               // navController.navigate(Route.PAGE_C+"/$param1/$param2")
                navController.navigate(Route.PAGE_C,
                   bundleOf(
                       "param1" to param1,
                       "param2" to param2
                   )
                )
            })
        }

        composable(
            Route.PAGE_C,
        ) { backStackEntry ->
            backStackEntry.arguments?.apply {
                val param1 =getString("param1")
                val param2 =getString("param2")
                PageC(param1,param2)
            }
        }

        composable(Route.PAGE_B) {
            PageB()
        }
    }

}

是不是代码清爽了许多,运行效果依然一样。

四、 底部导航栏如何实现跳转

4.0引入依赖: toml文件下新增:

compose-material = "1.6.0"

androidx-navigation-compose-runtime = { module = "androidx.compose.material:material", version.ref = "compose-material" }

app build.gradle新增

implementation(libs.androidx.navigation.compose.runtime)

新增HomePage、Project、Mine页面,界面都非常简单,就是一个Text就不在赘述了;修改界面D的代码,让界面D作为HomePage、Project、Mine的容器,通过BottomNavigation 来实现界面的跳转。

@Composable
fun  PageD(navController:NavHostController) {

   Scaffold(
       modifier = Modifier.fillMaxSize(),
       bottomBar = {
           BottomBar(navController)
       }
   ) { contentPadding->

       NavGraph(navController)
   }

}

val navBarItems = listOf(
    NavBarItem.Home,
    NavBarItem.Project,
    NavBarItem.Mine
)

sealed class NavBarItem(val label: String, val icon: Int, val route: String) {
    object Home : NavBarItem("首页", R.drawable.ic_tab_home, Route.HOME)
    object Project : NavBarItem("项目", R.drawable.ic_tab_project, Route.PROJECT)
    object Mine : NavBarItem("我的", R.drawable.ic_tab_mine, Route.MINE)
}



@Composable
fun BottomBar(navHostController: NavHostController){
    BottomNavigation(
        backgroundColor = MaterialTheme.colorScheme.primary
    ){
        val navBackStackEntry by navHostController.currentBackStackEntryAsState()
        val currentRoute = navBackStackEntry?.destination?.route
        navBarItems.forEach { item->
            BottomNavigationItem(
                selected = currentRoute == item.route,
                icon = {
                    Icon(
                        painter = painterResource(id = item.icon),
                        contentDescription = item.route,
                        modifier = Modifier
                            .size(22.dp)
                            .padding(bottom = 4.dp)
                    )
                },
                selectedContentColor = MaterialTheme.colorScheme.onPrimary,
                unselectedContentColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.3f),
                label = {
                    Text(
                        text = item.label,
                        fontSize = 12.sp
                    )
                },
                onClick = {
                   navHostController.navigate(item.route){
                       navHostController.graph.startDestinationRoute?.let { route->
                           popUpTo(route){
                               saveState=true
                           }
                       }
                       launchSingleTop=true
                       restoreState=true
                   }
                }
            )
        }
    }
}

NavGraph 文件中新增代码

@Composable
fun NavGraph(navController: NavHostController) {
    NavHost(
        navController = navController,
        startDestination = Route.PAGE_A
    ) {
        composable(Route.PAGE_A) {
            PageA(onButtonClick = {
                navController.navigate(Route.PAGE_B)
            }, enterPageC = {param1,param2->
               // navController.navigate(Route.PAGE_C+"/$param1/$param2")
                navController.navigate(Route.PAGE_C,
                   bundleOf(
                       "param1" to param1,
                       "param2" to param2
                   )
                )
            }, enterPageD = {
                navController.navigate(Route.PAGE_D)
            })
        }

        composable(
            Route.PAGE_C,
        ) { backStackEntry ->
            backStackEntry.arguments?.apply {
                val param1 =getString("param1")
                val param2 =getString("param2")
                PageC(param1,param2)
            }
        }

        composable(Route.PAGE_B) {
            PageB()
        }

        composable(Route.PAGE_D){
            PageD(navController)
        }

        composable(Route.HOME){
            HomePage()
        }
        composable(Route.PROJECT){
            Project()
        }
        composable(Route.MINE){
            Mine()
        }
    }

}

运行一下看看效果

java.lang.IllegalStateException: ViewModelStore should be set before setGraph call

WTF这时什么情况,经过查阅资料得知,NavHostController不允许嵌套,那么我们在程序中嵌套了么?嵌套了,PageD(NavHostController) 和NavGraph 共用了一个NavHostController,知道产生问题的原因就好改了

@Composable
fun  PageD() {
    val pageDNavController = rememberNavController()
   Scaffold(
       modifier = Modifier.fillMaxSize(),
       bottomBar = {
           BottomBar(pageDNavController)
       }
   ) { contentPadding->
      BottomNavGraph(pageDNavController)
   }

}

移除NavGraph中的二级代码

 composable(Route.HOME){
            HomePage()
        }
        composable(Route.PROJECT){
            Project()
        }
        composable(Route.MINE){
            Mine()
        }

新增BottomNavGraph代码

@Composable
fun BottomNavGraph(navController: NavHostController){
    NavHost(navController=navController, startDestination = Route.HOME){
        composable(Route.HOME){
            HomePage()
        }
        composable(Route.PROJECT){
            Project()
        }
        composable(Route.MINE){
            Mine()
        }
    }
}

在运行一下,运行结果与预期相符。