Jetpack之Room的使用,结合Flow"/>
Jetpack之Room的使用,结合Flow
本文主要还是参考官方文档,然后以保存搜索历史为例操作一波。
准备工作
Room
在SQLite
上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。
依赖
如需在应用中使用Room
,请将以下依赖项添加到应用的 build.gradle
文件。
dependencies {def room_version = "2.2.5"implementation "androidx.room:room-runtime:$room_version"kapt "androidx.room:room-compiler:$room_version"// optional - Kotlin Extensions and Coroutines support for Roomimplementation "androidx.room:room-ktx:$room_version"// optional - Test helperstestImplementation "androidx.room:room-testing:$room_version"
}
主要组件
数据库
:包含数据库持有者,并作为应用已保留的持久关系型数据的底层连接的主要接入点。
使用@Database
注释的类应满足以下条件:- 是扩展
RoomDatabase
的抽象类。 - 在注释中添加与数据库关联的实体列表。
- 包含具有 0 个参数且返回使用
@Dao
注释的类的抽象方法。
在运行时,您可以通过调用Room.databaseBuilder()
或Room.inMemoryDatabaseBuilder()
获取Database
的实例。
- 是扩展
Entity
:表示数据库中的表。DAO
:包含用于访问数据库的方法。
应用使用 Room 数据库来获取与该数据库关联的数据访问对象 (DAO)。然后,应用使用每个 DAO 从数据库中获取实体,然后再将对这些实体的所有更改保存回数据库中。 最后,应用使用实体来获取和设置与数据库中的表列相对应的值。
关系如图:
ok,基本概念了解之后,看一下具体是怎么搞的。
Entity
@Entity(tableName = "t_history")
data class History(/*** @PrimaryKey主键,autoGenerate = true 自增* @ColumnInfo 列 ,typeAffinity 字段类型* @Ignore 忽略*/@PrimaryKey(autoGenerate = true)@ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)val id: Int? = null,@ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT)val name: String?,@ColumnInfo(name = "insert_time", typeAffinity = ColumnInfo.TEXT)val insertTime: String?,@ColumnInfo(name = "type", typeAffinity = ColumnInfo.INTEGER)val type: Int = 1
)
- Entity对象对应一张表,使用
@Entity
注解,并声明你的表名即可 @PrimaryKey
主键,autoGenerate = true
自增@ColumnInfo
列,并声明列名 ,typeAffinity
字段类型@Ignore
声明忽略的对象
很简单的一张表,主要是name
和insertTime
字段。
DAO
@Dao
interface HistoryDao {//按类型 查询所有搜索历史@Query("SELECT * FROM t_history WHERE type=:type")fun getAll(type: Int = 1): Flow<List<History>>@ExperimentalCoroutinesApifun getAllDistinctUntilChanged() = getAll().distinctUntilChanged()//添加一条搜索历史@Insertfun insert(history: History)//删除一条搜索历史@Deletefun delete(history: History)//更新一条搜索历史@Updatefun update(history: History)//根据id 删除一条搜索历史@Query("DELETE FROM t_history WHERE id = :id")fun deleteByID(id: Int)//删除所有搜索历史@Query("DELETE FROM t_history")fun deleteAll()
}
- @Insert:增
- @Delete:删
- @Update:改
- @Query:查
这里有一个点需要注意的,就是查询所有搜索历史
返回的集合我用Flow
修饰了。
只要是数据库中的任意一个数据有更新,无论是哪一行数据的更改,那就重新执行 query
操作并再次派发Flow
。
同样道理,如果一个不相关的数据更新时,Flow
也会被派发,会收到与之前相同的数据。
这是因为 SQLite 数据库的内容更新通知功能是以表 (Table) 数据为单位,而不是以行 (Row) 数据为单位,因此只要是表中的数据有更新,它就触发内容更新通知。Room 不知道表中有更新的数据是哪一个,因此它会重新触发 DAO 中定义的 query 操作。您可以使用 Flow 的操作符,比如 distinctUntilChanged 来确保只有在当您关心的数据有更新时才会收到通知。
//按类型 查询所有搜索历史@Query("SELECT * FROM t_history WHERE type=:type")fun getAll(type: Int = 1): Flow<List<History>>@ExperimentalCoroutinesApifun getAllDistinctUntilChanged() = getAll().distinctUntilChanged()
数据库
@Database(entities = [History::class], version = 1)
abstract class HistoryDatabase : RoomDatabase() {abstract fun historyDao(): HistoryDaocompanion object {private const val DATABASE_NAME = "history.db"private lateinit var mPersonDatabase: HistoryDatabase//注意:如果您的应用在单个进程中运行,在实例化 AppDatabase 对象时应遵循单例设计模式。//每个 RoomDatabase 实例的成本相当高,而您几乎不需要在单个进程中访问多个实例fun getInstance(context: Context): HistoryDatabase {if (!this::mPersonDatabase.isInitialized) {//创建的数据库的实例mPersonDatabase = Room.databaseBuilder(context.applicationContext,HistoryDatabase::class.java,DATABASE_NAME).build()}return mPersonDatabase}}}
- 使用
@Database
注解声明 entities
数组,对应此数据库中的所有表version
数据库版本号
注意:
如果您的应用在单个进程中运行,在实例化 AppDatabase 对象时应遵循
单例设计模式
。 每个 RoomDatabase
实例的成本相当高,而您几乎不需要在单个进程中访问多个实例。
使用
在需要的地方获取数据库
mHistoryDao = HistoryDatabase.getInstance(this).historyDao()
获取搜索历史
private fun getSearchHistory() {MainScope().launch(Dispatchers.IO) {mHistoryDao.getAll().collect {withContext(Dispatchers.Main){//更新ui}}}}
collect
是Flow
获取数据的方式,并不是唯一方式,可以查看文档。
为什么放在协程
里面呢,因为数据库的操作是费时的,而协程可以轻松的指定线程,这样不阻塞UI
线程。
查看Flow源码也发现,Flow是协程包下的
package kotlinx.coroutines.flow
以collect为例,也是被suspend
修饰的,既然支持挂起
,那配合协程
岂不美哉。
@InternalCoroutinesApipublic suspend fun collect(collector: FlowCollector<T>)
保存搜索记录
private fun saveSearchHistory(text: String) {MainScope().launch(Dispatchers.IO) {mHistoryDao.insert(History(null, text, DateUtils.longToString(System.currentTimeMillis())))}}
清空本地历史
private fun cleanHistory() {MainScope().launch(Dispatchers.IO) {mHistoryDao.deleteAll()}}
作者:
数据库升级
数据库升级是一个重要的操作,毕竟可能会造成数据丢失,也是很严重的问题。
Room通过Migration
类来执行升级的操作,我们只要告诉Migration
类改了什么就行,比如新增
字段或表。
定义Migration类
/*** 数据库版本 1->2 t_history表格新增了updateTime列*/private val MIGRATION_1_2: Migration = object : Migration(1, 2) {override fun migrate(database: SupportSQLiteDatabase) {database.execSQL("ALTER TABLE t_history ADD COLUMN updateTime String")}}/*** 数据库版本 2->3 新增label表*/private val MIGRATION_2_3: Migration = object : Migration(2, 3) {override fun migrate(database: SupportSQLiteDatabase) {database.execSQL("CREATE TABLE IF NOT EXISTS `t_label` (`id` INTEGER PRIMARY KEY autoincrement, `name` TEXT)")}}
Migration
接收两个参数:
- startVersion 旧版本
- endVersion 新版本
通知数据库更新
mPersonDatabase = Room.databaseBuilder(context.applicationContext,HistoryDatabase::class.java,DATABASE_NAME).addMigrations(MIGRATION_1_2, MIGRATION_2_3).build()
完整代码
@Database(entities = [History::class, Label::class], version = 3)
abstract class HistoryDatabase : RoomDatabase() {abstract fun historyDao(): HistoryDaocompanion object {private const val DATABASE_NAME = "history.db"private lateinit var mPersonDatabase: HistoryDatabasefun getInstance(context: Context): HistoryDatabase {if (!this::mPersonDatabase.isInitialized) {//创建的数据库的实例mPersonDatabase = Room.databaseBuilder(context.applicationContext,HistoryDatabase::class.java,DATABASE_NAME).addMigrations(MIGRATION_1_2, MIGRATION_2_3).build()}return mPersonDatabase}/*** 数据库版本 1->2 t_history表格新增了updateTime列*/private val MIGRATION_1_2: Migration = object : Migration(1, 2) {override fun migrate(database: SupportSQLiteDatabase) {database.execSQL("ALTER TABLE t_history ADD COLUMN updateTime String")}}/*** 数据库版本 2->3 新增label表*/private val MIGRATION_2_3: Migration = object : Migration(2, 3) {override fun migrate(database: SupportSQLiteDatabase) {database.execSQL("CREATE TABLE IF NOT EXISTS `t_label` (`id` INTEGER PRIMARY KEY autoincrement, `name` TEXT)")}}}}
注意:
@Database
注解中版本号的更改,如果是新增表的话,entities
参数里也要添加上。
建议升级操作顺序
修改版本号 -> 添加Migration -> 添加给databaseBuilder
配置编译器选项
Room 具有以下注解处理器选项:
room.schemaLocation
:配置并启用将数据库架构导出到给定目录中的 JSON 文件的功能。如需了解详情,请参阅 Room 迁移。room.incremental
:启用 Gradle 增量注释处理器。room.expandProjection
:配置 Room 以重写查询,使其顶部星形投影在展开后仅包含 DAO 方法返回类型中定义的列。
android {...defaultConfig {...javaCompileOptions {annotationProcessorOptions {arguments += ["room.schemaLocation":"$projectDir/schemas".toString(),"room.incremental":"true","room.expandProjection":"true"]}}}
}
配置好之后,编译运行,module文件夹下会生成一个schemas
文件夹,其下有一个json
文件,里面包含数据库的基本信息。
{"formatVersion": 1,"database": {"version": 1,"identityHash": "xxx","entities": [{"tableName": "t_history","createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `insert_time` TEXT, `type` INTEGER NOT NULL)","fields": [{"fieldPath": "id","columnName": "id","affinity": "INTEGER","notNull": false},{"fieldPath": "name","columnName": "name","affinity": "TEXT","notNull": false},{"fieldPath": "insertTime","columnName": "insert_time","affinity": "TEXT","notNull": false},{"fieldPath": "type","columnName": "type","affinity": "INTEGER","notNull": true}],"primaryKey": {"columnNames": ["id"],"autoGenerate": true},"indices": [],"foreignKeys": []}],"views": [],"setupQueries": ["CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)","INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'xxx')"]}
}
ok,基本使用讲解完了,如果对你有用,点个赞呗 ^ _ ^
参考
- Room官方文档
- Room更新日志
- Flow官方文档
- 实战 | 在 Room 中使用 Flow
- 协程 Flow 最佳实践 | 基于 Android 开发者峰会应用
更多推荐
Jetpack之Room的使用,结合Flow
发布评论