Jetpack Compose实战教程(三)
第三章 实现一个带接口数据请求的界面
一、本章目标
功能还是很简单,界面上头部一个横向布局,加上一个请求接口之后得到的列表,那么直接开干。
友情提醒,如果各位看官有不懂的代码可以先看一下之前的章节,循序渐进,如果还是有不懂的,可以给我留言
二、开始编码
2.1 先把基本布局写好
class IncomeActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
contentUI()
}
}
@Composable
fun contentUI(){
BaseTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = Color.Black
) {
Box(Modifier.fillMaxSize()) {
Image(
painter = painterResource(id = R.mipmap.ic_common_bg),
contentDescription = null,
Modifier
.fillMaxWidth()
.fillMaxHeight()
)
Column {
Spacer(
//这里的DimenUtil和StatusBarUtils都是工具类,px2dp就是将px转dp的
//getStatusBarHeight就是获取状态栏的高度,大家可以自行百度找代码
modifier = Modifier.height(
DimenUtil.px2dp(this@IncomeActivity,
StatusBarUtils.getStatusBarHeight(this@IncomeActivity).toFloat()).dp)
)
title()
content()
}
}
}
}
}
@Composable
fun title(){
Box(Modifier.fillMaxWidth()){
Image(painter = painterResource(id = R.mipmap.ic_back_white2),
null,
modifier = Modifier
.size(48.dp)
.padding(12.dp)
.clickable {
finish()
}
)
Text(
text = "收益明细",
color = Color(0xffffffff),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.align(alignment = Alignment.Center)
)
}
}
@Composable
fun content(){
Column(
Modifier
.padding(16.dp, 16.dp, 16.dp, 0.dp)
.fillMaxSize()
.background(Color.Transparent)) {
}
}
@Preview(showBackground = true)
@Composable
fun previewUI(){
contentUI()
}
}
运行结果如图:
2.2 编写列表布局
//将所有的数据存放到一个列表里面
private val mData = mutableListOf<UnionIncomeBean>()
@Composable
fun setListData(){
LazyColumn(
Modifier
.fillMaxSize()
.background(Color.Transparent)
) {
//遍历数据列表
items(mData.size) {
Column {
Box(
Modifier
.fillMaxWidth()
.height(113.dp)
.padding(start = 16.dp, end = 16.dp)){
//这里下面的Image,我是根据UI效果,自己自定义实现的View,所以用了一下这种写法,pixelBlockDrawable的代码我就不贴了,各位看官可以直接用一张灰白色图片替代
Image(bitmap = drawableToBitamp(pixelBlockDrawable,
DimenUtil.getScreenWidth(this@IncomeActivity)-
DimenUtil.dp2px(this@IncomeActivity,32f),
DimenUtil.dp2px(this@IncomeActivity,113f)),
contentDescription = "" ,
modifier = Modifier.fillParentMaxSize())
//这里加载网络图片,我用到了coil的compose,如果发现AsyncImage没法使用的
//请先在build.gradle下面添加coil的compose依赖:api 'io.coil-kt:coil-compose:2.4.0'
AsyncImage(model = ImageRequest.Builder(LocalContext.current)
.data(mData[it].img)
.transformations(RoundedCornersTransformation(DimenUtil.dp2px(this@IncomeActivity,8f).toFloat()))
.scale(Scale.FILL)
.build(), contentDescription = "",
Modifier
.padding(top = 12.dp)
.width(56.dp)
.height(56.dp)
)
Text(text = mData[it].name, Modifier
.padding(top = 15.dp,start = 68.dp),
color = Color(0xffffffff),
fontSize = 16.sp
)
Text(text = "ID: ${mData[it].uid}",
Modifier.padding(start = 68.dp,top = 45.dp),
color = Color(0x99ffffff),
fontSize = 14.sp)
Row(
Modifier
.fillParentMaxWidth()
.padding(start = 12.dp, top = 80.dp)){
Text(text = "今日流水:",
color = Color(0x99ffffff),
fontSize = 12.sp
)
Text(text = mData[it].todayIncome,
Modifier.padding(start = 5.dp),
color = Color(0xffaffba1),
fontSize = 12.sp
)
Text(text = "今日流水:",
Modifier.padding(start = 16.dp),
color = Color(0x99ffffff),
fontSize = 12.sp
)
Text(text = mData[it].yesterdayIncome,
Modifier.padding(start = 5.dp),
color = Color(0xffaffba1),
fontSize = 12.sp
)
}
}
//每条渲染完毕之后,加个间距
Spacer(modifier = Modifier.height(12.dp))
}
}
}
}
然后将这个布局添加到内容中去
@Composable
fun content(){
Column(
Modifier
.padding(16.dp, 16.dp, 16.dp, 0.dp)
.fillMaxSize()
.background(Color.Transparent)) {
setListData()
}
}
接着我们请求接口获取一下数据,这里我模拟了请求接口,就用假数据生成了:
fun getData(){
for(i in 1..10){
mData.add(UnionIncomeBean("xxxxxx图片地址","测试房间$i",1183301+i,
"9999$i","4444$i"))
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
contentUI()
}
//在onCreate里面获取初始数据就可以了,发现了compose的好处了吗?先生成的UI,
//UI里面遍历了mData,那么只要mData的值改变,UI就会自动改变,这就是声明式UI
getData()
}
运行结果如图:
2.3 加载更多数据
上面显示了10条数据,但实际我们的开发场景中,往往会有更多数据,这里就像是分页,每页获取10条,当滑动到底部的时候,我们就得加载下一页,那么我们来改动一下代码。
//首先,将mutableListOf 改成mutableStateListOf,用于可记录数据状态
private val mData = mutableStateListOf<UnionIncomeBean>()
接着,在我们的布局列表里面使用状态
@Composable
fun setListData(){
//创建一个可记录状态的参数
val listState = rememberLazyListState()
//设置滑动监听事件
val scrollListener = remember {
object :NestedScrollConnection{
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
//当第一条可见的下标 + 界面上一共可见的item数量 >总数量-2时,就去再获取下一页的数据
//这里的判断基本表现为是滑动到倒数第三条时开始加载下一页
if(listState.firstVisibleItemIndex+listState.layoutInfo.visibleItemsInfo.size>mData.size-2){
getData()
}
return super.onPostScroll(consumed, available, source)
}
}
}
LazyColumn(
Modifier
.fillMaxSize()
.background(Color.Transparent)
.nestedScroll(scrollListener), //这里添加监听
state = listState //记录状态
) {
//.....上面的那些代码
}
}
运行结果如图:
可以看到我们已经可以无限加载数据了
2.4 没有更多数据
我们的数据总有加载完的一刻,那么当数据加载完毕之后,上下滚动一下列表又会去继续请求接口,这肯定不行,用户也没法感知没有更多数据了,所以我们要再改进一下代码
首先,我们添加一个参数来记录是否还有数据:
private var isNoMoreDataShow = mutableStateOf(false)
然后,我们为没有更多数据,添加一个UI布局,并且加入参数判断
@Composable
fun setListData(){
LazyColumn(
Modifier
.fillMaxSize()
.background(Color.Transparent)
.nestedScroll(scrollListener),
state = listState
) {
items(mData.size){
//....上面已经有了,省略掉这些代码
}
if(isNoMoreDataShow.value) { //当我们上面记录的状态为true,代表没有更多数据了
item {
Box(Modifier.fillMaxWidth().height(40.dp)) {
Text(
text = "没有更多数据啦",
modifier = Modifier.align(alignment = Alignment.Center),
color = Color(0x99ffffff),
fontSize = 16.sp
)
}
}
}
}
}
接下来我们假设加载完第二页就没有数据了
fun getData(){
//当数据达到20条之后,就不加载更多数据了,并且将isNoMoreDataShow设置为true
if(mData.size>=20){
isNoMoreDataShow.value =true
return
}
for(i in 1..10){
//省略代码
}
}
运行结果如图:
至此,我们实现了一个开发过程中常见的请求接口 并且列表显示数据的代码逻辑