Compose搭档 — Flow、Room

Compose如虎添翼 – 搭配Flow、Room!!!

Compose系列文章,请点原文阅读。原文:是时候学习Compose了!

通过上文Compose结合ViewModel、LiveData的示例,这次我们可以继续前进一步了。其实Room、Flow和Compose并没有直接关系,只是Flow可以转换为LiveData或者State,而LiveData和Compose的搭配我们前文已经说了。Jetpack Room又支持返回Flow类型的数据,单介绍Flow也没多少意思,所以索性加上Jetpack Room简单开发示例来丰富篇幅。

一、需求一览

这节的主题是搭配Kotlin Flow以及Jetpack Room来示例一款24节气应用,要求列表形式展示相关节气信息,图片来源于网络。同时更改Room数据库信息后需要及时将修改过的信息刷新并显示在页面上,大致的UI效果如下所示吧(自作多情的加了一个小的动画):
GIF 2021-5-26 16-38-32.gif
特别鸣谢:这节内容,用到了一些24节气的图片素材,感谢UI小姐姐:@雪莉莉Dribbble】【站酷】。

二、Compose UI开发

首先这节内容我们需要展示网络图片,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:<version>"
}

然后使用方式很简单,使用 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写完后,运行起来你应该就可以看到上图的效果了。【哦对了,忘了提醒你一句,网络权限!!!】

三、Room集成及使用

3.1、在使用kotlin编写的gradle脚本中集成

这里我使用的是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"
            }
        }
        
    }
  • room.schemaLocation:配置并启用将数据库架构导出到给定目录中的 JSON 文件的功能。
  • room.incremental:启用 Gradle 增量注解处理器。
  • room.expandProjection:配置 Room 以重写查询,使其顶部星形投影在展开后仅包含 DAO 方法返回类型中定义的列。

如果上述步骤丢失的话,编译的时候会报如下错误:
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.

3.2、使用方式

3.2.1、数据库表(Table)

数据库中的表,在这里我们可以将上述的 数据类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 = ""         //图片地址
)
  • @Entity 表示这个数据类对应数据库中的表,表名(tableName)为 “term”
  • @PrimaryKey 表示id字段为主键,自增
  • @ColumnInfo表示列相关信息,name是列名,typeAffinity是字段类型,文本、数字等
3.2.2、数据访问对象(DAO-DataAccessObjects )

数据库表有了,我们需要使用数据访问对象(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()
}
  • Query 表示查询,SELECT * FROM term 表示我们需要从term表中查出所有的数据;
  • Insert表示插入数据;

注意 :在queryAll()方法,我们返回的是Flow类型的数据;

3.2.3、数据库(Database)

为什么最后说数据库呢?因为Room中配置数据库我们需要表和数据访问对象,直接使用上述创建好的SolarTerm表和 SolarTermDao对象:

@Database(entities = [SolarTerm::class], version = 1)
abstract class AppDatabase : RoomDatabase() {

    //获得数据访问对象
    abstract fun getSolarTermDao(): SolarTermDao
}
3.2.4、添加节气数据到数据库

这里我们不考虑架构的问题了,仅仅示例在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栏,选中你的设备,稍等片刻后应该就能看到我们创建的了,如下所示:
Snipaste_2021-05-26_20-04-19.png

好了,这个时候我们把日志打印的面板悬浮出来放到了数据库面板的上面。接下来展示一波骚操作,直接更改数据库中表数据,我们把name名更改下,看看有什么效果:GIF 2021-5-26 20-13-24.gif

哇哦!这就是Room的强悍之处了,搭配Flow或者LiveData,修改数据库数据后观察者可以直接得到响应。(为什么id是从2开始的呢?因为我之前做实验添加过数据又删了,id是自增的,所以到2了!)

四、Compose + Flow

其实单纯的Room和Compose并没有关系,Flow才是我们要处理的问题,而Flow数据又可以通过 asLiveData() 转换为LiveData数据,又或者通过 collectAsState() 来直接转换为Compose所需的State数据,所以文章到这里就基本结束了,我们只需在之前的代码中将伪数据更改为相应的State数据即可,如下所示:

val termsState = terms.observeAsState(arrayListOf())

LazyColumn {
    items(termsState.value) {
        SolarTermsItem(entity = it)
    }
}

眼见为实,最后还是看下效果吧,左侧是AS中数据库的操作,右侧是模拟器上显示的UI:
GIF 2021-5-26 20-50-27.gif

你自己的增删改查同样会触发更新,这里我就偷懒不再演示了。

五、小结

写了这么一大片文章,其实就是一个Flow转LiveData的 asLiveData() 方法以及Flow转State的 collectAsState() 方法,还有一个图片加载库accompanist-coil。Room这里我们接触的只是冰山一角,还有很多等着我们去学习和探索,加油啊!

以上代码仅仅作为示例使用,代码格式和规范都不敢恭维,万万不可用于开发。

  • 15
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 20
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值