Compose系列文章,请点原文阅读。原文:是时候学习Compose了!
通过上文Compose结合ViewModel、LiveData的示例,这次我们可以继续前进一步了。其实Room、Flow和Compose并没有直接关系,只是Flow可以转换为LiveData,而LiveData和Compose的搭配我们前文已经说了。Jetpack Room又支持返回Flow类型的数据,单介绍Flow也没多少意思,所以索性加上Jetpack Room简单开发示例来丰富篇幅。
这节的主题是搭配Kotlin Flow以及Jetpack Room来示例一款24节气应用,要求列表形式展示相关节气信息,图片来源于网络。同时更改Room数据库信息后需要及时将修改过的信息刷新并显示在页面上,大致的UI效果如下所示吧(自作多情的加了一个小的动画):
特别鸣谢:这节内容,用到了一些24节气的图片素材,感谢UI小姐姐:@雪莉莉 【Dribbble】【站酷】。
首先这节内容我们需要展示网络图片,Compose加载网络图片需要一个工具库 Image Loadding。请参考官方主页:https://google.github.io/accompanist/,这些工具库提供了有图片加载、类似ViewPager、下拉刷新等的功能,大家可以择需获取。我们只需要一个图片加载库,官方给了Glide和Coil的示例,我们使用后者做演示。
在build.gradle中添加依赖,accompanist-coil的版本请和Compose的版本对应,否则编译会出现错误。目前示例:Compose是1.0.0-beta07版本,accompanist-coil是0.10.0版本:
repositories {
mavenCentral()
}
dependencies {
implementation "com.google.accompanist:accompanist-coil:"
}
然后使用方式很简单,使用 painter: Painter 参数,代码如下(Preview模式下无法预览到网络图片):
Image(
painter = rememberCoilPainter(
request = "https://picsum.photos/300/300",
),
contentDescription = "Desc",
)
既然要写列表,那么肯定是先写ItemView,图片的问题已经搞定了,还剩一个动画效果:点击ItemView,节气介绍内容向下弹出。其实也很简单,用到了官方还在实验阶段的一个API - AnimatedVisibility,该可组合函数需要一个boolean类型的参数,就可以实现默认的弹出收回的效果,直接看代码:
@ExperimentalAnimationApi
@Composable
fun SolarTermsItem(entity: SolarTerm) {
val isShowDetail = remember {
mutableStateOf(false)
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(
onClick = {
isShowDetail.value = !isShowDetail.value
}
)
) {
//图片
Image(
painter = rememberCoilPainter(
request = entity.imgUrl,
),
contentDescription = "image of term",
modifier = Modifier
.fillMaxWidth(0.5f)
.wrapContentHeight()
.clip(RoundedCornerShape(10))
)
//右侧节气信息
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 8.dp)
) {
Text(
text = entity.name,
fontSize = 24.sp,
fontWeight = FontWeight.ExtraBold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = entity.time,
fontSize = 16.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = entity.mark,
fontSize = 14.sp,
fontWeight = FontWeight.Normal
)
}
}
//点击item,向下弹出节气的内容介绍,再次点击收回
AnimatedVisibility(visible = isShowDetail.value) {
Text(
text = entity.content,
fontSize = 14.sp,
fontWeight = FontWeight.Normal,
modifier = Modifier.padding(top = 8.dp)
)
}
}
}
OK,ItemView写好了,我们先造两条伪数据,如下,然后使用LazyColumn就可以打造出一个竖向的列表了,不需要Adapter不需要LayoutManager,对比之前的开发方式简直能给我乐哭了:
val entityYuShui = SolarTerm(
name = "雨水",
content = "正月中,天一生水,春始属木,然生木者必水也,故立春后继之雨水。且东风既解冻,则散而为雨矣。",
time = "2.18-2.19",
mark = "春雨贵如油",
imgUrl = "https://cdn.dribbble.com/users/2676519/screenshots/7056971/media/efde75ba876f38cb95ce5b936f481784.jpg?compress=1&resize=1000x750"
)
val entityChunFen = SolarTerm(
name = "春分",
content = "春分者,阴阳相半也。故昼夜均而寒暑平。斗指壬为春分,约行周天,南北两半球昼夜均分,又当春之半,故名为春分。",
time = "3.20-3.21",
mark = "春分有雨到清明,清明下雨无路行",
imgUrl = "https://cdn.dribbble.com/users/2676519/screenshots/7056971/media/cdcf98372abf88a554a53362c6037f2a.jpg?compress=1&resize=1000x750"
)
val list = arrayListOf(entityYuShui, entityChunFen)
LazyColumn {
items(list) {
SolarTermsItem(entity = it)
}
}
UI写完后,运行起来你应该就可以看到上图的效果了。【哦对了,忘了提醒你一句,网络权限!!!】
这里我使用的是kotlin来编写Gradle构建脚本的,基于groovy的请直接参考【官网示例】,build.gradle.kts脚本文件添加依赖如下,注意添加kotlin-kapt插件依赖,此处room版本为2.3.0:
plugins {
...
id("kotlin-kapt")
}
dependencies {
...
//room相关依赖内容
implementation ("androidx.room:room-runtime:${rootProject.extra["room_version"]}")
// To use Kotlin annotation processing tool (kapt)
kapt ("androidx.room:room-compiler:${rootProject.extra["room_version"]}")
// optional - Kotlin Extensions and Coroutines support for Room
implementation ("androidx.room:room-ktx:${rootProject.extra["room_version"]}")
// optional - Test helpers
testImplementation ("androidx.room:room-testing:${rootProject.extra["room_version"]}")
}
别以为这就配置完了,还有很重要的一个步骤 – 配置注解处理器选项:
android {
defaultConfig {
//...
javaCompileOptions {
annotationProcessorOptions {
arguments["room.schemaLocation"] = "$projectDir/schemas"
arguments["room.incremental"] = "true"
arguments["room.expandProjection"] = "true"
}
}
}
如果上述步骤丢失的话,编译的时候会报如下错误:
Schema export directory is not provided to the annotation processor so we cannot export the schema. You can either provide room.schemaLocation
annotation processor argument OR set exportSchema to false.
数据库中的表,在这里我们可以将上述的 数据类SolarTerm 用相关注解来表示:
原数据类:
data class SolarTerm(
val name: String = "", //节气名:春分
val content: String = "", //简介
val time: String = "", //节气时间:3.20-3.21
val mark: String = "", //相关俗语:春分有雨到清明,清明下雨无路行
val imgUrl: String = "" //图片地址
)
添加相关注解:
@Entity(tableName = "term")
data class SolarTerm(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)
val id: Int? = null, //主键
@ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT)
val name: String = "", //节气名:春分
val content: String = "", //简介
val time: String = "", //节气时间:3.20-3.21
val mark: String = "", //相关俗语:春分有雨到清明,清明下雨无路行
val imgUrl: String = "" //图片地址
)
数据库表有了,我们需要使用数据访问对象(DAO)来访问该表中的内容,Room也提供了@Dao的注解:
@Dao
interface SolarTermDao {
/**
* 查询所有的节气数据
*/
@Query("SELECT * FROM term")
fun queryAll(): Flow<List<SolarTerm>>
/**
* 插入一条数据
*/
@Insert()
fun insert(entity: SolarTerm)
/**
* 清空term表中所有内容
*/
@Query("DELETE FROM term")
fun delete()
}
注意 :在queryAll()方法,我们返回的是Flow类型的数据;
为什么最后说数据库呢?因为Room中配置数据库我们需要表和数据访问对象,直接使用上述创建好的SolarTerm表和 SolarTermDao对象:
@Database(entities = [SolarTerm::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
//获得数据访问对象
abstract fun getSolarTermDao(): SolarTermDao
}
这里我们不考虑架构的问题了,仅仅示例在Activity中如何使用数据库(db)及数据访问对象(DAO):
//获取到数据库
val db = Room
.databaseBuilder(applicationContext, AppDatabase::class.java, "db_solar_terms")
.build()
//获取到数据访问对象
val solarTermDao = db.getSolarTermDao()
//查询所有数据并转换为LiveData类型
val terms = solarTermDao.queryAll().asLiveData()
//观察数据变化
terms.observe(this, {
var temp = ""
for (term in it) {
temp += "${
term.name} - ${
term.time} \n"
}
Log.e("存储的数据", temp)
})
val entityYuShui = SolarTerm(
name = "雨水",
content = "正月中,天一生水,春始属木,然生木者必水也,故立春后继之雨水。且东风既解冻,则散而为雨矣。",
time = "2.18-2.19",
mark = "春雨贵如油",
imgUrl = "https://cdn.dribbble.com/users/2676519/screenshots/7056971/media/efde75ba876f38cb95ce5b936f481784.jpg?compress=1&resize=1000x750"
)
//在线程中操作数据库
Thread {
solarTermDao.insert(entityYuShui)
}.start()
在获取节气数据列表的时候我们是用了**asLiveData()**将Flow类型的数据转换为了LiveData类型,然后在activity中直接监听该数据的变化。
然后我们新开线程去操作数据访问对象(注意数据库操作需要在子线程中),添加了一条数据到表中。运行上述代码,在控制台应该会打印出如下日志:
存储的数据: 雨水 - 2.18-2.19
然后我们打开下方App Inspection栏,选中你的设备,稍等片刻后应该就能看到我们创建的库和表了,如下所示:
好了,这个时候我们把日志打印的面板悬浮出来放到了数据库面板的上面。接下来展示一波骚操作,直接更改数据库中表数据,我们把name名更改下,看看有什么效果:
哇哦!这就是Room的强悍之处了,搭配Flow或者LiveData,修改数据库数据后观察者可以直接得到响应。(为什么id是从2开始的呢?因为我之前做实验添加过数据又删了,id是自增的,所以到2了!)
其实单纯的Room和Compose并没有关系,Flow才是我们要处理的问题,而Flow数据又可以通过 asLiveData() 转换为LiveData数据,所以文章到这里就基本结束了,我们只需在之前的代码中将伪数据更改为相应的State数据即可,如下所示:
val termsState = terms.observeAsState(arrayListOf())
LazyColumn {
items(termsState.value) {
SolarTermsItem(entity = it)
}
}
眼见为实,最后还是看下效果吧,左侧是AS中数据库的操作,右侧是模拟器上显示的UI:
你自己的增删改查同样会触发更新,这里我就偷懒不再演示了。
写了这么一大片文章,其实就是一个Flow转LiveData的asLiveData()方法。哦对了,还有一个图片加载库accompanist-coil。Room这里我们接触的只是冰山一角,还有很多等着我们去学习和探索,加油啊!
以上代码仅仅作为示例使用,代码格式和规范都不敢恭维,万万不可用于开发。