相关文章推荐
踢足球的饺子  ·  setProgressBar - ...·  2 月前    · 
活泼的奔马  ·  aiohttp ...·  7 月前    · 
聪明的麦片  ·  解决Python unknown ...·  1 年前    · 
斯文的佛珠  ·  jMeter 里 CSV Data Set ...·  1 年前    · 
奋斗的企鹅  ·  CIR,CBS,EBS,PIR,PBS傻傻分 ...·  1 年前    · 

「本文已参与好文召集令活动,点击查看: 后端、大前端双赛道投稿,2万元奖池等你挑战!

Jetpack Compose出来有一段时间了,一直都没有去尝试,这次有点想法去玩一玩这个声明性界面工具,就以“原神”为主题写个列表吧。

整体设计参考 DisneyCompose

因为数据比较简单,也就只包含图片、姓名、描述等。所以在后台数据存储上选择的是Bmob后端云,一个方便前端开发的后端服务平台。

主要数据也是从原神各大网站搜集下来的,新建表结构并且将数据填充,我们简单看一下Bmob的后台。

数据准备好了,那就开始我们的Compose之旅。

首页UI绘制

从上面的项目效果图来看,首页总布局属于是一个网格列表,平分两格,列表中的每个Item上方带有头像,头像下面是角色名称以及角色其他信息。

因为整体分成两列,所以选择的是网格布局,Compose提供了一个实现- LazyVerticalGrid

fun LazyVerticalGrid(
    cells: GridCells,
    modifier: Modifier = Modifier,
    state: LazyListState = rememberLazyListState(),
    contentPadding: PaddingValues = PaddingValues(0.dp),
    content: LazyGridScope.() -> Unit

LazyVerticalGrid中有几个重要参数先说明一下:

  • GridCells :主要控制如何将单元格构建为列,如GridCells.Fixed(2),表示两列平分。
  • Modifier : 主要用来对列表进行额外的修饰。
  • PaddingValues :主要设置围绕整个内容的padding。
  • LazyListState :用来控制或观察列表状态的状态对象
  • 首页布局是平分两列的网格布局,那相应的代码如下:

    LazyVerticalGrid(cells = GridCells.Fixed(2)) {}
    

    单个Item

    看过了外部框架,那现在来看每个Item的布局。每个Item为卡片式,外边框为圆角,且带有阴影。内部上方是一张图片Image,图片下方是两行文字Text。那Item具体该怎样布局?

    我们先来看看在Compose之前,在xml中是怎么写?例如使用ConstraintLayout布局,顶部放一个ImageView,再来一个TextView layout_constraintTop_toBottomOf ImageView,最后在来个TextViewTopToBottomOf第一个TextView

    那使用Compose应该怎么写?

    其实在Compose里也存在着ConstraintLayout布局并且具体Api的调用思路与在xml中使用也是一致的。我们就来看看具体操作。

    ConstraintLayout() {
    Image()
    Text()
    Text()
    

    一共两个元素:ImageText,分别代表着xml里的ImageViewTextView

  • Image:
  • Image(
        painter = rememberCoilPainter(request = item.url),
        contentDescription = "",
        contentScale = ContentScale.Crop,
        modifier = Modifier
               .clickable(onClick = {
                      val objectId = item.objectId
                      navController.navigate("detail/$objectId")
               .padding(0.dp, 4.dp, 0.dp, 0.dp)
               .width(180.dp)
               .height(160.dp)
               .constrainAs(image) {
                     centerHorizontallyTo(parent)
                     top.linkTo(parent.top)
    

    Image加载的是网络图片,则使用painter加载图片链接,contentScale与xml中的scaleType相似,modifier主要设置图片的样式,点击事件、宽高等。里面有一个需要注意的点constrainAs(image)

    constrainAs(image) {
                            centerHorizontallyTo(parent)
                            top.linkTo(parent.top)
    

    这段代码主要表示Image在父布局中的位置,例如相对父布局,相对其他子控件等,有点xml中layout_constraintTop_toBottomOf内味。下面Text也是相同的道理。

    Text(text = item.name,
                    color = Color.Black,
                    style = MaterialTheme.typography.h6,
                    textAlign = TextAlign.Center,
                    modifier = Modifier
                        .padding(0.dp, 4.dp, 0.dp, 0.dp)
                        .constrainAs(title) {
                            centerHorizontallyTo(parent)
                            top.linkTo(image.bottom)
    

    Text的设置主要包含Text内容、文字类型、大小、颜色等。在constrainAs(title)里有一句top.linkTo(image.bottom),这句代码指的就是xml中,TextView layout_constraintTop_toBottomOf ImageView

    在Image和Text中发现了一个点,constrainAs(?)中传入了一个值,且设置相对位置时也是以此值为控件的代表。这是在进行相对位置的设定之前,利用createRefs创建多个引用,在ConstraintLayout中作为Modifier.constrainAs的一部分分配给布局。

    val (image, title, content) = createRefs()
    

    具体代码:

    ConstraintLayout() {
                val (image, title, content) = createRefs()
                Image(
                    //图片地址
                    painter = rememberCoilPainter(request = item.url),
                    contentDescription = "",
                    //图片缩放规则
                    contentScale = ContentScale.Crop,
                    modifier = Modifier
                        .clickable(onClick = {//点击事件
                            val objectId = item.objectId
                            navController.navigate("detail/$objectId")
                        .padding(0.dp, 4.dp, 0.dp, 0.dp)
                        .width(180.dp)
                        .height(160.dp)
                        .constrainAs(image) {
                            centerHorizontallyTo(parent)  //水平居中
                            top.linkTo(parent.top)//位于父布局的顶部
                Text(text = item.name,
                    color = Color.Black,//颜色
                    style = MaterialTheme.typography.h6,//字体格式
                    textAlign = TextAlign.Center,
                    modifier = Modifier
                        .padding(0.dp, 4.dp, 0.dp, 0.dp)
                        .constrainAs(title) {
                            centerHorizontallyTo(parent)//水平居中
                            top.linkTo(image.bottom)//位于图片的下方
                Text(text = item.from,
                    color = Color.Black,
                    style = MaterialTheme.typography.body1,
                    textAlign = TextAlign.Center,
                    modifier = Modifier
                        .padding(4.dp)
                        .constrainAs(content) {
                            centerHorizontallyTo(parent)
                            top.linkTo(title.bottom)
    

    UI已经画好了,接下来就是数据展示的事情。还是以ViewModel-LiveData-Repository为整体请求方式。 因为数据都存储到了Bmob后台,就直接使用Bmob的方式查询数据:

    private val bmobQuery: BmobQuery<GcDataItem> = BmobQuery()
    fun queryRoleData(successLiveData: MutableLiveData<List<GcDataItem>>) {
            bmobQuery.findObjects(object : FindListener<GcDataItem>() {
                override fun done(list: MutableList<GcDataItem>?, e: BmobException?) {
                    if (e == null) {
                        successLiveData.value = list
    

    具体的请求方式可参考Bmob的完档,这里就不在赘述。 ViewModel中还是抛出一个LiveData,而UI层相对之前有一些变化。

    @OptIn(ExperimentalFoundationApi::class)
    @Composable
    fun HomePoster(navController: NavController, model: HomeViewModel = viewModel()) {
        model.queryGcData()
        val data: List<GcDataItem> by model.getDataLiveData().observeAsState(listOf())
        LazyVerticalGrid(cells = GridCells.Fixed(2)) {
            items(data) {
                ItemPoster(navController, item = it)
    

    Compose提供了一个viewModel()方法来获取ViewModel实例,至于怎么拿到数据,Compose提供了LiveData的一个扩展方法 observeAsState(listOf()) 。它的主要作用是用来观察这个LiveData,并通过State表示它的值,每次有新值提交到LiveData时,返回的状态将被更新,从而导致每个状态的重新组合。

    拿到List数据后,网格LazyVerticalGrid就开始使用items(data){}添加列表,

     LazyVerticalGrid(cells = GridCells.Fixed(2)) {
            items(data) {
                ItemPoster(navController, item = it)
    

    而ItemPoster就是我们设置Item布局的地方,将每个Item的数据传递给ItemPoster,利用Image、Text等控件设置imageUrl、text内容等。

    @Composable
    fun ItemPoster(navController: NavController, item: GcDataItem) {
        Surface(
            modifier = Modifier
                .padding(4.dp),
            color = Color.White,
            elevation = 8.dp,
            shape = RoundedCornerShape(8.dp)
            ConstraintLayout() {
                val (image, title, content) = createRefs()
                Image(
                    //设置图片Url-item.url
                    painter = rememberCoilPainter(request = item.url),
                  Text(text = item.name
                  Text(text = item.from
    

    样例中还有一个从列表跳转到详情页的功能,Compose提供了一个跳转组件-navigation。这个navigation与之前管理Fragment的navigation思路也是一致的,利用NavHostController进行不同页面的管理。我们先使用 rememberNavController()方法创建一个NavHostController实例。

    val navController = rememberNavController()
    

    接着将navController与NavHost相关联,且设置导航图的起始目的地startDestination

     NavHost(navController = navController, startDestination = "Home") {}
    

    我们将起始目的地暂时先标记为“Home”。 那如何对页面进行管理?这就需要在NavHost中使用composable添加页面,例如该项目有两个页面,一个首页列表页,一个详情页。我们就可以这样写:

     NavHost(
                navController = navController, startDestination = "Home"
                composable(
                    route = "Home",
                    HomePoster(navController)
                composable("detail/{objectId}"){
                    val objectId = it.arguments?.getString("objectId")
                    DetailPoster(objectId){
                        navController.popBackStack()
    

    第一个composable则代表的是列表页,并且将到达目的地的路线route设置为“Home”,其实类似于ARouter框架中在每个Activity上设置Path,做一个标识作用,后面做跳转时也是依据该route进行跳转。

    第二个composable则代表的是详情页,同样设置route="detail"

    那如何从列表页跳到详情页?只需要在点击事件里使用navController.navigate("detail"),传入想要跳转的route即可。

    携带参数跳转

    因为详情页需要根据所点击列表Item的Id进行数据查询,点击时要将id传到详情页,这就需要携带参数。 在Compose中,向route添加参数占位符,如"detail/{objectId}",从composable()函数提取 NavArguments。 如下修改详情页:

     composable("detail/{objectId}"){
                    val objectId = it.arguments?.getString("objectId")
                    DetailPoster(objectId){
                        navController.popBackStack()
    

    跳转时将objectId传到route的占位符中即可。

    clickable(onClick = {
              val objectId = item.objectId
              navController.navigate("detail/$objectId")})
    

    当然,compose navigation还支持launchMode设置、深层链接等,具体可查看官方文档

    对于用习惯了xml编写UI的我来说,首次上手Compose其实还是蛮不习惯,Compose打破了原有的格局,给了我们一个全新的视角去看待Android,学完后有种“哦,原来UI还可以这么干!!”的感叹。对于Android开发者来说,其实需要这些新的路线去突破自己的固有化思维。

    Compose的风格其实和Flutter有点像,估计是出于同一个爸爸的原因。但是Compose没有Flutter的无限套娃,对Android开发者来说还是比较友好的。如果想要学习Flutter,可以用Compose作为过渡。

    以上便是本篇内容,感谢阅读,如果对你有帮助,欢迎点赞收藏关注三连走一波👉

    项目地址:genshin-compose

    分类:
    Android
    标签: