结合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")
    	......
    }

二、应用介绍和实体类
为了说明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)定义滚动列表的每一单项定义在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)
            }
         }
    }
}

(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)
            }
        }
    }
}

(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)
    }
}

为了在更好识别和处理这些不同的界面,定义密封类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","机器人详细信息")
}

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

/**
 * 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()
        }
    }
}

到目前位置,导航还未实现,这是因为在导航控制中缺乏导航控制器对象来处理导航动作,而且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()
        }
    }
}

在上述代码中,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) //将导航控制器对象传递给单项视图
            }
        }
    }
}

(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.传递基本类型的数据
假设从滚动列表跳转到机器人详细信息界面传递的是字符串,修改机器人列表单项视图界面,增加点击动作的发送数据的处理:
(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)
            }
        }
    }
}

(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)
        }
    }
}

(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)
    }
}

经过这样的处理,导航到机器人详细信息界面如下所示:
在这里插入图片描述
图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)
        }
    }
}

(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)
    }
}

会抛出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'
 	...
 }

(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)
            }
        }
    }
}

(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)
            }
        }
    }
}

代码没有发生变化
(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)
    }
}

(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对象
        }
    }
}

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

class MainActivity : ComponentActivity() {
    
    
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContent {
    
    
            Ch04_ComposeTheme {
    
    
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
    
    
                    NavigationGraphScreen()
                }
            }
        }
    }
}

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

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

猜你喜欢

转载自blog.csdn.net/userhu2012/article/details/127601476