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