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导航动画WebView浮动按钮底部导航