• 结合Navigation组件实现JetPack Compose的界面导航


    Android JetPack Compose可以利用Navigation组件来实现导航
    一、Navigation组件的配置
    新建项目,选择Empty Compose Activity。
    然后,在项目模块的build.gradle设置如下内容:

    
    	dependencies {
        	def nav_version = "2.5.2"
        	implementation("androidx.navigation:navigation-compose:$nav_version")
        	......
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    二、应用介绍和实体类
    为了说明JetPack Compose组件的导航应用,定义一个简单的应用:即显示一个机器人滚动列表,然后点击滚动列表中的某个单项,进入具体某个机器人界面。
    机器人列表
    图1:机器人列表
    通过点击列表的某行的机器人图标,进入下面的界面。
    在这里插入图片描述

    图2:显示单独机器人信息

    为此创建一个表示机器人实体的类,定义如下:

    data class Robot(val imageId:Int,val name:String,val description:String):Parcelable{
        constructor(parcel: Parcel) : this(
            parcel.readInt(),
            parcel.readString()!!,
            parcel.readString()!!
        ) {
        }
    
        override fun writeToParcel(parcel: Parcel, flags: Int) {
            parcel.writeInt(imageId)
            parcel.writeString(name)
            parcel.writeString(description)
        }
    
        override fun describeContents(): Int =0
    
        companion object CREATOR : Parcelable.Creator<Robot> {
            override fun createFromParcel(parcel: Parcel): Robot {
                return Robot(parcel)
            }
            override fun newArray(size: Int): Array<Robot?> {
                return arrayOfNulls(size)
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    三、定义不同的界面
    在本应用中定义三个界面:
    (1)定义滚动列表的每一单项定义在RobotItemView

    /**
     * 定义列表单项的视图
     * @param robot Robot
     */
    @Composable
    fun RobotItemView(robot:Robot){
        Column{
            Row(modifier= Modifier
                .fillMaxWidth()
                .border(1.dp,Color.Black)
                .clip(RoundedCornerShape(10.dp))
                .background(colorResource(id = R.color.teal_200))
                .padding(5.dp)){
                Image(modifier = Modifier
                    .width(80.dp)
                    .height(80.dp)
                    .clip(shape = CircleShape)
                    .background(Color.Black)
                    .clickable {
                        //增加导航处理
                    },
                    painter = painterResource(id = robot.imageId),
                    contentDescription = "机器人")
                Column{
                    Text("${robot.name}",fontSize=16.sp,color=Color.Blue)
                    Text("${robot.description}",fontSize=20.sp,color=Color.DarkGray)
                }
             }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    (2)定义一个显示机器人滚动列表的界面RobotListScreen

    /**
     * Robot list screen
     * 定义显示机器人滚动列表的界面
     */
    @Preview
    @Composable
    fun RobotListScreen(){
        val robots = mutableListOf<Robot>()
        for(i in 1..20)
            robots.add(Robot(android.R.mipmap.sym_def_app_icon,"第${i}机器人","进入机器人世界"))
        var reverseLayout = false
        Box(modifier= Modifier
            .background(Color.Black)
            .fillMaxSize()){
            LazyColumn(state= rememberLazyListState(),
                verticalArrangement = if(!reverseLayout) Arrangement.Top else Arrangement.Bottom){
                items(robots){robot->
                    RobotItemView(robot = robot)
                }
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    (3)定义具体的机器人信息的界面RobotScreen

    /**
     * 定义机器人具体信息显示界面
     * @param robot Robot
     */
    @Composable
    fun RobotScreen(){
        val robot = Robot(android.R.mipmap.sym_def_app_icon,"第1号机器人","第1号机器人进入机器人的世界")
    
        Column(modifier = Modifier
            .background(Color.Black)
            .padding(20.dp)
            .fillMaxSize(),
            verticalArrangement = Arrangement.Center){
            Row(verticalAlignment = Alignment.CenterVertically){
                Image(modifier= Modifier
                    .width(160.dp)
                    .height(160.dp),
                    painter= painterResource(id = robot.imageId),
                    contentDescription = "${robot.description}")
                Text("${robot.name}",fontSize=36.sp,color=Color.White)
            }
            Text("${robot.description}",fontSize=24.sp,color=Color.Yellow)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    为了在更好识别和处理这些不同的界面,定义密封类Screens来创建各自界面的实体对象:

    /**
     * 定义可显示的界面
     * @property route String 导航线路名称
     * @property title String 界面标题
     * @constructor
     */
    sealed class Screens(val route:String,val title:String){
        object HomePage:Screens("home","机器人列表")
        object RobotPage:Screens("robot","机器人详细信息")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    四、在不同的界面实现导航切换
    为了实现导航,需要定义导航图,标明各个界面之间的导航方向,为此:

    /**
     * Navigation graph screen
     * 定义导航图
     */
    @Composable
    fun NavigationGraphScreen(){
        //获取导航控制器
        val navController = rememberNavController()
        NavHost(navController, startDestination = Screens.HomePage.route){
            composable(Screens.HomePage.route){
                RobotListScreen()
            }
            composable(Screens.RobotPage.route){
                RobotScreen()
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    到目前位置,导航还未实现,这是因为在导航控制中缺乏导航控制器对象来处理导航动作,而且RobotItemView的对Image点击动作定义为空,所以是不可能实现导航;
    因此做出如下修改:
    (1)重新定义导航图

    /**
     * Navigation graph screen
     * 定义导航图
     */
    @Preview
    @Composable
    fun NavigationGraphScreen(){
        //获取导航控制器
        val navController = rememberNavController()
        NavHost(navController, startDestination = Screens.HomePage.route){
            composable(Screens.HomePage.route){
                RobotListScreen(navController)
            }
            composable(Screens.RobotPage.route){
                RobotScreen()
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在上述代码中,NavHost是导航图,是NavController的容器,指定了导航的起点路线,即Screens.HomePage.route

    (2)修改RobotListScreen,增加导航控制器,使之具有界面导航的能力

    /**
     * Robot list screen
     * 定义显示机器人滚动列表的界面
     */
    @Composable
    fun RobotListScreen(navController:NavController){//增加导航控制器对象
        val robots = mutableListOf<Robot>()
        for(i in 1..20)
            robots.add(Robot(android.R.mipmap.sym_def_app_icon,"第${i}机器人","进入机器人世界"))
        var reverseLayout = false
        Box(modifier= Modifier
            .background(Color.Black)
            .fillMaxSize()){
            LazyColumn(state= rememberLazyListState(),
                verticalArrangement = if(!reverseLayout) Arrangement.Top else Arrangement.Bottom){
                items(robots){robot->
                    RobotItemView(navController ,robot = robot) //将导航控制器对象传递给单项视图
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    (3)修改RobotItemView,将导航图中导航控制器对象作为参数传递给它,增加导航处理:

    /**
     * 定义列表单项的视图
     * @param robot Robot
     */
    @Composable
    fun RobotItemView(navController:NavController,robot:Robot){
        Column{
            Row(modifier= Modifier
                .fillMaxWidth()
                .border(1.dp, Color.Black)
                .clip(RoundedCornerShape(10.dp))
                .background(colorResource(id = R.color.teal_200))
                .padding(5.dp)){
                Image(modifier = Modifier
                    .width(80.dp)
                    .height(80.dp)
                    .clip(shape = CircleShape)
                    .background(Color.Black)
                    .clickable {
                        //增加导航处理
                        //根据导航路线robot到Screens.RobotPage对应的RobotScreen定义的界面
                        navController.navigate("robot")
                    },
                    painter = painterResource(id = robot.imageId),
                    contentDescription = "机器人")
                Column{
                    Text("${robot.name}",fontSize=16.sp,color=Color.Blue)
                    Text("${robot.description}",fontSize=20.sp,color=Color.DarkGray)
                }
             }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    到目前位置实现了从机器人滚动列表的选择指定单项的图标,跳转到下一个页面。但是由于每次跳转都是一个具有相同数据的界面,并不符合实际情况。实际情况需要完成的是从滚动列表中选择一个单项的图标,点击后进入这个“机器人”的详细信息的界面,因此需要传递相应的数据。

    五、在导航中传递数据
    1.传递基本类型的数据
    假设从滚动列表跳转到机器人详细信息界面传递的是字符串,修改机器人列表单项视图界面,增加点击动作的发送数据的处理:
    (1)修改RobotItemScreen函数,增加发送数据的处理

    @Composable
    fun RobotItemView(navController:NavController,robot:Robot){
        Column{
            Row(modifier= Modifier
                .fillMaxWidth()
                .border(1.dp, Color.Black)
                .clip(RoundedCornerShape(10.dp))
                .background(colorResource(id = R.color.teal_200))
                .padding(5.dp)){
                Image(modifier = Modifier
                    .width(80.dp)
                    .height(80.dp)
                    .clip(shape = CircleShape)
                    .background(Color.Black)
                    .clickable {
                        //增加导航处理,发送方在导航路线中发送字符串数据
                        navController.navigate("robot/${robot.toString()}")
                    },
                    painter = painterResource(id = robot.imageId),
                    contentDescription = "机器人")
                Column{
                    Text("${robot.name}",fontSize=16.sp,color=Color.Blue)
                    Text("${robot.description}",fontSize=20.sp,color=Color.DarkGray)
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    (2)修改导航图
    修改导航图,在导航图中为数据的接收方指定参数名称和参数类型,并修改导航路线为带参数的形式:

    /**
     * Navigation graph screen
     * 定义导航图
     */
    @Preview
    @Composable
    fun NavigationGraphScreen(){
        //获取导航控制器
        val navController = rememberNavController()
        NavHost(navController, startDestination = Screens.HomePage.route){
            //数据的发送方
            composable(Screens.HomePage.route){
                RobotListScreen(navController)
            }
            //数据的接收方
            composable(route=Screens.RobotPage.route+"/{robot}",//修改导航路线,增加要传递的参数名称
            arguments=listOf(navArgument("robot"){type= NavType.StringType}))//指定接收的参数和参数类型
            {
                val robotStr=it.arguments?.getString("robot")?:"没有任何信息,接收参数失败"
                RobotScreen(robotStr)
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    (3)在接收方的机器人详细信息的界面是接收方,指定接收参数和参数类型的处理,代码如下所示:

    /**
     * 定义机器人具体信息显示界面
     * @param robot Robot
     */
    @Composable
    fun RobotScreen(robot:String){
        Column(modifier = Modifier
            .background(Color.Black)
            .padding(20.dp)
            .fillMaxSize(),
            verticalArrangement = Arrangement.Center){
            Row(verticalAlignment = Alignment.CenterVertically){
                Image(modifier= Modifier
                    .width(160.dp)
                    .height(160.dp),
                    painter= painterResource(id = android.R.mipmap.sym_def_app_icon),
                    contentDescription = "${robot}")
            }
            Text("${robot}",fontSize=24.sp,color=Color.Yellow)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    经过这样的处理,导航到机器人详细信息界面如下所示:
    在这里插入图片描述
    图3:接收传递的字符串数据

    2.传递自定义类型的数据
    在实际情况中,往往需要传递自定义类型的对象数据,如上述的Robot这个实现Parcelable接口的类型的对象时该怎么办?如果直接将这样的对象传递给下一个界面,形式如下:
    (1)修改导航图
    将导航图中的接收方的参数类型修改为Robot类型

    @Preview
    @Composable
    fun NavigationGraphScreen(){
        //获取导航控制器
        val navController = rememberNavController()
        NavHost(navController, startDestination = Screens.HomePage.route){
            //数据的发送方
            composable(Screens.HomePage.route){
                RobotListScreen(navController)
            }
            //数据的接收方
            composable(route=Screens.RobotPage.route+"/{robot}",//修改导航路线,增加要传递的参数名称
            arguments=listOf(navArgument("robot"){
                 //指定接收的参数和参数类型 
                type= NavType.inferFromValueType(Robot(R.mipmap.ic_launcher,"",""))}))
            {
                val robot:Robot=it.arguments?.getParcelable("robot")?:Robot(android.R.mipmap.sym_def_app_icon,"测试","机器人信息获取失败")
                RobotScreen(robot)
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    (2)修改接收数据方的界面
    发送数据方的界面仍保持上述的内容,无需修改,只需要修改接收方的界面处理,将接收方RobotScreen函数接收的参数类型从字符串修改为Robot类型,GUI界面做出相应的处理即可,如下代码所示:

    @Composable
    fun RobotScreen(robot:Robot){//修改参数类型为Robot
        Column(modifier = Modifier
            .background(Color.Black)
            .padding(20.dp)
            .fillMaxSize(),
            verticalArrangement = Arrangement.Center){
            Row(verticalAlignment = Alignment.CenterVertically){
                Image(modifier= Modifier
                    .width(160.dp)
                    .height(160.dp),
                    painter= painterResource(id = robot.imageId),
                    contentDescription = "${robot.description}")
                Text("${robot.name}",fontSize=36.sp,color=Color.White)
            }
            Text("${robot.description}",fontSize=24.sp,color=Color.Yellow)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    会抛出java.lang.UnsupportedOperationException: Parcelables don’t support default values.
    因为Parcelable类型的数据是不支持默认值。如果直接传递会抛出不支持操作的异常。因此需要其他方式来传递自定义类型的对象。
    3. 利用Gson实现自定义数据的传递
    其中一个解决方法就是在发送方将自定义类型对象的数据转换成JSON形式的字符串,然后在接收方将接收的字符串再转换成自定义类型的对象,从而达到传递数据的目的。在这里借助Gson框架来实现。
    (1)增加Gson依赖
    需要在模块的build.gradle中增加Gson框架的依赖,形式如下:

    dependencies {
     	implementation 'com.google.code.gson:gson:2.10'
     	...
     }
    
    • 1
    • 2
    • 3
    • 4

    (2)滚动列表的单项视图的定义

    /**
     * 定义列表单项的视图
     * @param robot Robot
     */
    @Composable
    fun RobotItemView(navController:NavController,robot:Robot){
        Column{
            Row(modifier= Modifier
                .fillMaxWidth()
                .border(1.dp, Color.Black)
                .clip(RoundedCornerShape(10.dp))
                .background(colorResource(id = R.color.teal_200))
                .padding(5.dp)){
                Image(modifier = Modifier
                    .width(80.dp)
                    .height(80.dp)
                    .clip(shape = CircleShape)
                    .background(Color.Black)
                    .clickable {
                        val robotStr = Gson().toJson(robot)
                        //增加导航处理,发送方在导航路线中发送字符串数据
                        navController.navigate("robot/${robotStr}")
                    },
                    painter = painterResource(id = robot.imageId),
                    contentDescription = "机器人")
                Column{
                    Text("${robot.name}",fontSize=16.sp,color=Color.Blue)
                    Text("${robot.description}",fontSize=20.sp,color=Color.DarkGray)
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    (3)修改滚动列表界面的定义

    /**
     * Robot list screen
     * 定义显示机器人滚动列表的界面
     */
    @Composable
    fun RobotListScreen(navController:NavController){
        val robots = mutableListOf<Robot>()
        for(i in 1..20)
            robots.add(Robot(android.R.mipmap.sym_def_app_icon,"第${i}机器人","第${i}机器人进入机器人世界"))
        var reverseLayout = false
        Box(modifier= Modifier
            .background(Color.Black)
            .fillMaxSize()){
            LazyColumn(state= rememberLazyListState(),
                verticalArrangement = if(!reverseLayout) Arrangement.Top else Arrangement.Bottom){
                items(robots){robot->
                    RobotItemView(navController,robot = robot)
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    代码没有发生变化
    (4)接收数据方界面的定义

    @Composable
    fun RobotScreen(robot:Robot){
        Column(modifier = Modifier
            .background(Color.Black)
            .padding(20.dp)
            .fillMaxSize(),
            verticalArrangement = Arrangement.Center){
            Row(verticalAlignment = Alignment.CenterVertically){
                Image(modifier= Modifier
                    .width(160.dp)
                    .height(160.dp),
                    painter= painterResource(id = robot.imageId),
                    contentDescription = "${robot.description}")
                Text("${robot.name}",fontSize=36.sp,color=Color.White)
            }
            Text("${robot.description}",fontSize=24.sp,color=Color.Yellow)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    (5)修改导航图

    @Preview
    @Composable
    fun NavigationGraphScreen(){
        //获取导航控制器
        val navController = rememberNavController()
        NavHost(navController, startDestination = Screens.HomePage.route){
            //数据的发送方
            composable(Screens.HomePage.route){
                RobotListScreen(navController)
            }
            //数据的接收方
            composable(route=Screens.RobotPage.route+"/{robot}",//修改导航路线,增加要传递的参数名称
            arguments=listOf(navArgument("robot"){
                type= NavType.StringType}))//指定接收的参数和参数类型为字符串
            {
                val robotJsonStr=it.arguments?.getString("robot")?:"接收错误的参数"
                RobotScreen(Gson().fromJson(robotJsonStr,Robot::class.java))//将字符串转换成Robot对象
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在主活动中调用导航图的界面,代码如下:

    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                Ch04_ComposeTheme {
                    Surface(
                        modifier = Modifier.fillMaxSize(),
                        color = MaterialTheme.colors.background
                    ) {
                        NavigationGraphScreen()
                    }
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    最后的运行结果如下所示:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    这时点击任意滚动列表单项图标,可以进入到指定的界面。
    参考文献
    使用Compose进行导航 https://developer.android.google.cn/reference/androidx/navigation/NavHost?hl=zh-cn

  • 相关阅读:
    Redis 缓存过期淘汰策略
    新版首途影视视频网站源码/22套带后台版全开源+无加密源码(全新二开完整版)
    如何创建前端绘图和图表?
    好用的PDF编辑软件有哪些?这几款工具建议收藏
    2022年朝阳区科技创新课之“产品创新与成果转化”训练营活动圆满结束
    【译】在调试时轻松导航代码委托
    【分享】教你加速访问GitHub,进来学!
    【机器学习】朴素贝叶斯概率模型
    cadence virtuoso layout drc error
    APScheduler-调度器 BackgroundScheduler
  • 原文地址:https://blog.csdn.net/userhu2012/article/details/127601476