Android Compose 新闻App下拉刷新复杂数据网格布局文字样式

Posted 初学者-Study

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Compose 新闻App下拉刷新复杂数据网格布局文字样式相关的知识,希望对你有一定的参考价值。

Compose 新闻App(四)下拉刷新、复杂数据、网格布局、文字样式

前言

  在上一篇文章中我们进行数据的存储和缓存的使用,这里我们进一步去优化这个业务。

正文

  首先我们想一个问题,那就是假如我一天不只是请求一次网络接口呢?要怎么办呢?难道我去应用管理中去清除本地数据然后再打开应用吗?那太傻了,那么就可以通过刷新的方式去更新当前的数据,同时这个数据还能存到本地数据库,这个业务看起来就更人性化一些。

一、下拉刷新

  通过标题就知道我要说什么内容了,在之前的android开发中下拉刷新是常用的功能,而在Compose中也如此,只不过使用方式更简单一些,首先我们添加依赖版本。

① 添加依赖

打开项目的build.gradle,增加如下版本代码:

accompanist_version = '0.24.4-alpha'

目前我们只会用accompanist库中的刷新组件,到后面我们可能会用到accompanist库中的其他组件,因此我这里定义了accompanist_version,当然你也可以不定义。
增加位置如下图所示:

然后打开app下的build.gradle,增加如下依赖代码:

implementation "com.google.accompanist:accompanist-swiperefresh:$accompanist_version"

增加位置如下图所示:

② 使用

  使用其实非常的简单,下面我们改动一下MainActivity.kt中的BodyContent()函数,如下图所示:

原来这里只有一个LazyColumn,现在我在它的上面增加了一个SwipeRefresh,然后里面有两个必备的属性值,state和onRefresh,state是表示刷新状态,onRefresh表示刷新后执行的操作,这里我就弹一个Toast。下面我们运行一下看看:

③ 样式更改

刚才是最基本的使用,功能基本上有了,那么我们改一下它的样式。

这里配置了一指示器的样式:refreshTriggerDistance表示刷新触发的距离,scale表示刷新View的大小动画,默认是关闭的,我们打开。然后就是背景颜色,还有就是形状样式,下面再看看运行的效果:

我这里是放的很慢去进行的,为的就是看清楚这个动画效果。

二、刷新数据

  现在对于下拉刷新控件上的说明就结束了,我们要进入使用的环节了,实际上使用就是把onRefresh中执行方法换成我们实际的业务逻辑就行了,只不过通过下拉刷新来串联这个业务。

刷不刷新数据需要一个变量来控制,因此首先我们需要改动EpidemicNewsRepository.kt中的getEpidemicNews()函数,如下图所示:

这里就是增加一个参数,把这个参数作为是否需要请求网络数据的标准之一,当前没有刷新并且不是今天第一次请求网络,则从本地获取,如果有刷新,就从网络中请求数据。
然后我们需要改一下MainViewModel.kt

原来这里里面只有一行代码现在则多了一些,这里的代码很好理解,这里暴露一个方法给外部去调用,当getNews()函数的参数有改变时,就会触发repository.getEpidemicNews(it),然后页面上继续观察result的变化,有变化就会更新页面数据。

下面回到MainActivity.kt中,修改一下initData()函数中的方法,如下图所示:

这里我去掉了那个没有什么必要的临时变量,下面我们只需要在BodyContent()函数中的onRefresh的函数体中调用viewModel.getNews(true)方法即可实现下拉刷新功能。

下面我们再运行一下:

OK,这个地方就完成了。

三、复杂数据

  在实际开发中,很多都是A表中含有B表,而如果不需要建立那么多表,我们可以通过Room去处理,例如Desc数据类

它里面含有GlobalStatistics和ForeignStatistics,我们要在数据库中添加的话那么怎么样去操作呢?可以让这两个数据类成为Desc数据类的列。添加@Embedded注解,同时给Desc加上@Entity注解,同时主键也要添加。

下面我们再看GlobalStatistics和ForeignStatistics,他们里面的字段大部分相同,而在Room中不运行字段相同的情况,因此我们需要改一下列名,代码如下所示:

data class GlobalStatistics(
    @ColumnInfo(name = "global_currentConfirmedCount")
    val currentConfirmedCount: Int = 0,
    @ColumnInfo(name = "global_confirmedCount")
    val confirmedCount: Int = 0,
    @ColumnInfo(name = "global_curedCount")
    val curedCount: Int = 0,
    @ColumnInfo(name = "global_currentConfirmedIncr")
    val currentConfirmedIncr: Int = 0,
    @ColumnInfo(name = "global_confirmedIncr")
    val confirmedIncr: Int = 0,
    @ColumnInfo(name = "global_curedIncr")
    val curedIncr: Int = 0,
    @ColumnInfo(name = "global_deadCount")
    val deadCount: Int = 0,
    @ColumnInfo(name = "global_deadIncr")
    val deadIncr: Int = 0,
    @ColumnInfo(name = "global_yesterdayConfirmedCountIncr")
    val yesterdayConfirmedCountIncr: Int = 0
)

data class ForeignStatistics(
    @ColumnInfo(name = "foreign_currentConfirmedCount")
    val currentConfirmedCount: Int = 0,
    @ColumnInfo(name = "foreign_confirmedCount")
    val confirmedCount: Int = 0,
    @ColumnInfo(name = "foreign_curedCount")
    val curedCount: Int = 0,
    @ColumnInfo(name = "foreign_currentConfirmedIncr")
    val currentConfirmedIncr: Int = 0,
    @ColumnInfo(name = "foreign_suspectedIncr")
    val suspectedIncr: Int = 0,
    @ColumnInfo(name = "foreign_confirmedIncr")
    val confirmedIncr: Int = 0,
    @ColumnInfo(name = "foreign_curedIncr")
    val curedIncr: Int = 0,
    @ColumnInfo(name = "foreign_deadCount")
    val deadCount: Int = 0,
    @ColumnInfo(name = "foreign_deadIncr")
    val deadIncr: Int = 0,
    @ColumnInfo(name = "foreign_suspectedCount")
    val suspectedCount: Int = 0
)

因为Desc这里只是一个数据类,而不是List,所以我们每次保存和添加数据都是用同一个指定的ID。那么我们就需要将Desc的id改为var,这样后面我们就直接改这个id。

@PrimaryKey var id: Int = 0,

数据表改好之后,还有一个简单的方法,我们去dao包下增加一个DescDao接口,里面的代码如下:

@Dao
interface DescDao 

    @Query("SELECT * FROM `desc` WHERE id LIKE :id LIMIT 1")
    suspend fun getDesc(id: Int = 1): Desc

    @Insert
    suspend fun insert(desc: Desc?)

    @Query("DELETE FROM `desc`")
    suspend fun deleteAll()

这里很好理解,查询的时候就查询id=1的数据,那么我们在插入数据的时候就要设置desc的id为1, 最后在AppDatabase中配置

  看到这里是不是觉得很奇怪,这样肯定会报错,为什么呢?因为我之前最开始添加了一个表,之前的版本就是1,那么我这里新增了Desc表,那么对应的数据库版本就要迁移,你可以理解为升级,同时我们需要去添加一个Migrations,在这里去写新增数据表的SQL,这是很麻烦的,那么在调试中如果你只是自己调试而不需要发给别人的话,你可以这样做,就是把你现在的应用卸载,你再重新安装就可以了。省时省力,但是如果你是线上的项目那你还是老老实实的写迁移数据库的SQL,并且做好测试,当然了我们现在这样做只是方便开发测试,实际中不推荐你这么做,切记,切记。

下面回到EpidemicNewsRepository中,修改一下saveNews()函数,代码如下:

这里我们直接拿到newslistItem,因为news和desc都是在newslistItem中,注意看在插入desc的时候将id设置为1,那么我们在查询的时候要怎么做呢?修改一下getLocalForNews()函数,

你会发现我这里并没有传入参数,因为用了缺省值,当然了你也可以传入一个其他值,那么你在出入的时候也要做更改。

现在数据这一块就可以了,下面我们来做UI这一块的内容,需要用到Desc表中的数据。

四、复杂列表

① 更改返回数据

在之前主页面中就是显示一个数据列表,而没有其他的东西了,我们需要的desc和news属于同一级,因此我们需要上一级的数据。那么就需要改一下

看这个图应该就很好理解了,我们先拿到NewslistItem,然后再通过NewslistItem拿到news和desc,然后在BodyContent()函数中增加一个参数。

好了,到这里我们的数据就到了它应该去的地方,下面我们打印一下:

这里的Gson在前面就已经添加了依赖库,没有注意到的看前面的内容或者看源码,下面运行一下,看有没有日志打印:


很好,有日志,那么说明数据库没有问题,下面进行数据的显示。

② 增加item

下面我们在之前的列表上方再增加一个item,来看看怎么增加。

首先我们增加了一个item,然后再item里面去设置一个Card,然后设置宽度、高度、内填充、阴影、背景颜色。也就是说在最上方增加一个卡片式布局,下面我们来看布局里面的内容。

在Card中有一个Row,那么里面的内容就是横向,然后Row里面放了两个Column,表示里面有两个纵向,两个Column的设置一样,这里要注意的是weight(1f),表示权重,现在两个都是Column都是1f就是各占Row的50%,Column里面的其他两个配置就是Colum的内容上下居中。

那么我们再来看Column里的内容

这里的就很好理解了,基本上不用说什么了,如果你需要知道这些currentConfirmedIncr的含义,就去天行的API上去看,哪里有,下面我们运行一下:

你会看到一个列表有两个内容,内容不一样,但还是同一个列表,并且你的下拉刷新一样有效。

③ 嵌套

这里显示两个内容有点少,我们改一下

上面截图中的代码如下:

item 
        Card(
            modifier = Modifier
                .fillMaxWidth()
                .padding(8.dp),
            elevation = 2.dp,
            backgroundColor = Color.White
        ) 
            Column 
                Row(modifier = Modifier.padding(12.dp)) 
                    Column(
                        modifier = Modifier.fillMaxSize().weight(1f),
                        verticalArrangement = Arrangement.Center,//设置垂直居中对齐
                        horizontalAlignment =  Alignment.CenterHorizontally//设置水平居中对齐
                    ) 
                        Text(text = "现存确诊人数")
                        Text(text = desc.currentConfirmedCount.toString(), fontSize = 24.sp, fontWeight = FontWeight.Bold)
                        Text(text = "较昨日 $desc.currentConfirmedIncr")
                    

                    Column(
                        modifier = Modifier.fillMaxSize().weight(1f),
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment =  Alignment.CenterHorizontally
                    ) 
                        Text(text = "累计确诊人数")
                        Text(text = desc.confirmedCount.toString(), fontSize = 24.sp, fontWeight = FontWeight.Bold)
                        Text(text = "较昨日 $desc.currentConfirmedIncr")
                    
                
                Row(modifier = Modifier.padding(12.dp)) 
                    Column(
                        modifier = Modifier.fillMaxSize().weight(1f),
                        verticalArrangement = Arrangement.Center,//设置垂直居中对齐
                        horizontalAlignment =  Alignment.CenterHorizontally//设置水平居中对齐
                    ) 
                        Text(text = "累计治愈人数")
                        Text(text = desc.curedCount.toString(), fontSize = 24.sp, fontWeight = FontWeight.Bold)
                        Text(text = "较昨日 $desc.curedIncr")
                    

                    Column(
                        modifier = Modifier.fillMaxSize().weight(1f),
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment =  Alignment.CenterHorizontally
                    ) 
                        Text(text = "累计死亡人数")
                        Text(text = desc.deadCount.toString(), fontSize = 24.sp, fontWeight = FontWeight.Bold)
                        Text(text = "较昨日 $desc.deadIncr")
                    
                
            
        
    

我们把固定高度改了,让它自适应里面的内容高度,下面再运行一下:

如果这个代码写在BodyContent()函数中,那么会看起来代码很多,我们可以抽离一下,新增一个descItem函数,代码如下图所示:

五、网格布局

  从上面这里的代码我们已经实现了功能,但是会不会看上去不太智能呢?如果每一次添加都这样,那就太蠢了,因此我们可以用到网格布局。Compose的网格布局有横向的有纵向的,但还不稳定,因此就需要手动去写,这里可以这样去写,首先在MainActivity.kt中创建两个数据类

data class DescItem(var title: String, var current: Int, var yesterday: Int)

data class GroupItem(val descItem: DescItem?,val isEmpty: Boolean)

我们实际需要的数据并不太多,然后我们可以写一个descItemPlus()函数

private fun LazyListScope.descItemPlus(desc: Desc) 


首先因为数据是组装的,不是列表所以手动构建一个列表。

private fun LazyListScope.descItemPlus(desc: Desc) 
    //构建一个DescItemList
    val descList = mutableListOf<DescItem>().apply 
        add(DescItem("现存确诊人数", desc.currentConfirmedCount, desc.currentConfirmedIncr))
        add(DescItem("累计确诊人数", desc.confirmedCount, desc.confirmedIncr))
        add(DescItem("累计治愈人数", desc.curedCount, desc.curedIncr))
        add(DescItem("累计死亡人数", desc.deadCount, desc.deadIncr))
        add(DescItem("现存无症状人数", desc.seriousCount, desc.seriousIncr))
    

现在数据源就有了,然后就是根据这个数据源去计算网格中的行和列。在descItemPlus函数中增加如下代码:

	//网格Items
    val gradItems = mutableListOf<List<GroupItem>>()
    var index = 0
    //网格是行与列组成,显示2列
    val columnNum = 2
    //计算显示几行
    val rowNum = ceil(descList.size.toFloat() / columnNum).toInt()
    //遍历行
    for (i in 0 until rowNum) 
        val rowItems = mutableListOf<GroupItem>()
        //遍历列
        for (j in 0 until columnNum) 
            if (index.inc() <= descList.size) 
                rowItems.add(GroupItem(descList[index++],false))
            
        

        //如果未填充满,则显示占位
        val itemEmpty = columnNum - rowItems.size
        for (j in 0 until itemEmpty) 
            rowItems.add(GroupItem(null,true))
        
        gradItems.add(rowItems)
    

最后在descItemPlus()函数中我们显示每一个item。

	//显示数据
    items(gradItems)  gradItem ->
        Row 
            for (gird in gradItem) 
                if (gird.isEmpty) 
                    Box(modifier = Modifier.weight(1f))
                 else 
                    Box(modifier = Modifier.weight(1f)) 
                        Card(
                            modifier = Modifier
                                .fillMaxWidth()
                                .padding(8.dp),
                            elevation = 2.dp,
                            backgroundColor = Color.White
                        ) 
                            val descItem = gird.descItem!!
                            Column(
                                modifier = Modifier.padding(12.dp),
                                verticalArrangement = Arrangement.Center,//设置垂直居中对齐
                                horizontalAlignment = Alignment.CenterHorizontally//设置水平居中对齐
                            ) 
                                Text(text = descItem.title)
                                Text(
                                    text = descItem.current.toString(),
                                    fontSize = 24.sp,
                                    fontWeight = FontWeight.Bold
                                )
                                Text(text = "较昨日 $descItem.yesterday")
                            
                        
                    
                
            
        
    

这样就可以实现功能了,下面我们调用一下:

然后运行一下:

实际中根据自己的需求去更改使用的方式,,这个descItemPlus()函数的代码会保留,这里我还是用之前的descItem()函数,因为我需要去更改文字的不同样式。

六、修改样式

首先在colors.xml中增加一些色值

	<color name="red">#FC3538</color>
    <color name="dark_red">#B00000</color>
    <color name="green">#1BB394</color>
    <color name="gray_black">#656565</color>
    <color name="gray">#989898</color>

下面我们来修改一下descItem()函数中的控件样式。

这里先来改第一个,这里修改了文字的大小,然后设置了颜色,和填充,最主要的是下面这个buildAnnotatedString,它可以对一个Text中的不同内容做不同的样式设置,然后这里还有一个拓展函数addSymbols(),代码如下:

fun Int.addSymbols(): String = if (this > 0.0 && this != 0) "+$this" else "$this"

可以直接写在MainActivity.kt,也可以单独建一个kotlin文件去写,这个拓展函数主要就是如果我们的int数据是正数,还要大于零,就添加一个+,负责就是原来的数据,返回String。那么其他的都可以照着改一下,我这里贴一下descItem()函数的代码:

private fun LazyListScope.descItem(desc: Desc) 
    item 
        Card(
            modifier = Modifier
                .fillMaxWidth()
                .padding(8.dp),
            elevation = 2.dpandroid新闻客户端问题

Android Compose 新闻App网络框架搭建

Android Compose 新闻App抽屉布局动态权限拍照返回

Android Compose 新闻App导航动画WebView浮动按钮底部导航

Android Compose 新闻App抽屉布局动态权限拍照返回

Android Compose 新闻App抽屉布局动态权限拍照返回