在之前的文章中我介绍了使用RxJava配合Room给自己的APP添加数据库支持,但随着技术的发展,现在已经有很多人开始使用kotlin开发,我的新项目也直接使用kotlin语言开发,如何在kotlin中方便的使用Room也成了当下的一个需求。Room也是支持kotlin的,接下来我就来介绍一下我是如何在kotlin中封装使用Room的。
Android官方Room配合RxJava接入及使用经验
本文会以一个日志系统为示例为各位同学展示kotlin中使用Room的方法。
当前使用的环境:
Android Studio 4.1.3
kotlin 1.4.31
Room 2.3.0
测试时间 2021-05-19
一、添加依赖
在写这篇文章的时候Room最新版本为2.3.0,我们就直接使用最新版本构建工程。因为使用的是kotlin,所以引用的库也相应的转换为kotlin库。项目中使用kotlin协程代替原来的RxJava,所以需要引用androidx.room:room-ktx
。因为需要使用kotlin注解库,记得在插件中配置id 'kotlin-kapt'
。
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
dependencies {
//......
//room
def room_version = "2.3.0"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// room针对kotlin协程功能的扩展库
implementation "androidx.room:room-ktx:$room_version"
}
二、创建Entity
在kotlin中创建Entity和Java差不多,也是创建一个数据模型给Room使用,不同的是Room支持kotlin的data class,我们可以写更少的代码去创建模型,但我更倾向于使用普通的class。
在kotlin中没有访问修饰符的变量默认为public,同时kotlin会自动为其创建get/set方法,Room需要使用get/set方法。
以下的两种写法效果一致,都是用来创建一个LogEntity
- data class 格式
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "log")
data class LogEntity(
@PrimaryKey(autoGenerate = true)
var id: Int = 0,
var time: Long = 0,
var type: String = "",
var code: Int = 0,
var message: String = "",
) {
constructor(type: String, code: Int, message: String) : this() {
time = System.currentTimeMillis()
this.type = type
this.code = code
this.message = message
}
}
- class 格式
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "log")
class LogEntity() {
@PrimaryKey(autoGenerate = true)
var id: Int = 0
var time: Long = 0
var type: String = ""
var code: Int = 0
var message: String = ""
constructor(type: String, code: Int, message: String) : this() {
time = System.currentTimeMillis()
this.type = type
this.code = code
this.message = message
}
}
为了方便我之后创建日志对象,在写数据模型的时候特意加了第二种构造方法,这样在我创建日志对象的时候,就只需要传递三个参数了。
三、创建Dao
此处我们使用kotlin的协程来处理异步查询逻辑,不需要再包装一个观察者来接收数据了。
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
@Dao
interface LogDao {
@Insert
suspend fun save(vararg logs: LogEntity): List
@Query("select time from log order by time asc limit 1")
suspend fun getFirstLogTime(): Long
@Query("select time from log order by time desc limit 1")
suspend fun getLastLogTime(): Long
@Query("select * from log where time>=:startTime and time <=:endTime")
suspend fun getLogByFilter(startTime: Long, endTime: Long): List
suspend fun getLogList(startTime: Long = 0, endTime: Long = 0): List {
val start = if (startTime == 0L) {
getFirstLogTime()
} else {
startTime
}
val end = if (endTime == 0L) {
getLastLogTime()
} else {
endTime
}
return getLogByFilter(start, end)
}
}
我在LogDao
中添加了四个数据库方法,分别对应存储日志、获取第一个日志的时间、获取最后一个日志的时间、根据时间筛选获取日志列表。
同时,为了方便获取日志列表,我添加了一个方法,代替手动获取日志时间再自动根据时间查询数据库,如果用户没有选择筛选时间也是可以自动查询的。
使用协程后可以更直观的看到方法返回的对象类型,但使用协程方法需要在协程的作用域中,创建协程作用域比较简单的两个方法是:
runBlocking {
//会阻塞当前线程的协程语句块
}
GlobalScope.launch {
//异步执行的协程语句块
}
四、创建DataBase
在kotlin中创建DataBase和在Java中创建DataBase类似,只是语法稍有不同。
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(version = 1, exportSchema = false, entities = [LogEntity::class])
abstract class LogDatabase : RoomDatabase() {
val logDao: LogDao by lazy { createLogDao() }
abstract fun createLogDao(): LogDao
}
按照官方要求,创建一个抽象方法即可使用,而我还定义了一个logDao
变量,同时利用kotlin的懒加载机制对其进行了初始化,这样做的好处是我们可以在首次使用的时候才创建这个对象且只创建一次,而且可以像使用一个对象一样去使用它。
(直接使用方法创建对象的同学也不用担心会创建多次,通过查看Room为Dao生成的代码可以发现,Room会帮你维护一个唯一的引用,不会重复创建对象)
Database注解中的entities属性需要传入一个数组,后期entity多了,直接在后面加上就好了。
五、 添加DatabaseManager
创建一个DatabaseManager类,该类用于管理数据库连接对象及数据库升级操作。
import android.app.Application
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
object DatabaseManager {
private const val DB_NAME = "logData.db"
private val MIGRATIONS = arrayOf(Migration1)
private lateinit var application: Application
val db: LogDatabase by lazy {
Room.databaseBuilder(application.applicationContext, LogDatabase::class.java, DB_NAME)
.addCallback(CreatedCallBack)
.addMigrations(*MIGRATIONS)
.build()
}
fun saveApplication(application: Application) {
DatabaseManager.application = application
}
private object CreatedCallBack : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
//在新装app时会调用,调用时机为数据库build()之后,数据库升级时不调用此函数
MIGRATIONS.map {
it.migrate(db)
}
}
}
private object Migration1 : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
// 数据库的升级语句
// database.execSQL("")
}
}
}
得益于kotlin对于单例的简单实现,我们只需要把标志类的class
换成object
就可以确保当前类是一个单例对象,实实在在的提高了程序员的效率。
我在这个管理类中同样利用懒加载的方式定义了一个名为db
的数据库对象,之后需要调用数据库的时候直接使用这个对象即可。
后面的CreatedCallBack
和Migration1
也是使用object
关键字定义为单例,方便使用。其中CreatedCallBack
用于初始创建数据库的回调,而Migration1
被放到了名为MIGRATIONS
的数组中,在数据库需要升级的时候会被调用。后续还有数据库升级操作时只需要创建一个新的升级类并放到数组中即可。
MIGRATIONS
相关的代码只是示例代码,在数据库的第一个版本时是不需要的。我是为了减少后续数据库升级迭代时代码的改动量而特意封装的,放到上面,供大家借鉴。
我在管理类中添加了saveApplication
方法,用来将application
存储下来,在之后懒加载生成对象的时候使用,如果不需要在APP启动的时候就使用数据库,这样做可以节省APP的启动时间。一定要在使用前调用saveApplication
方法,否则会出现空指针~
六、 使用Room
经过以上的准备工作,现在数据库的功能已经可以直接使用了。
- 插入日志数据
runBlocking {
val log1 = LogEntity("test", 1, "this is a test log")
val log2 = LogEntity("test", 1, "this is a test log")
try {
val ids = DatabaseManager.db.logDao.save(log1, log2)
println("insert number = ${ids.size}")
ids.map {
println("insert id = $it")
}
} catch (exception: Exception) {
println("insert error = ${exception.message}")
exception.printStackTrace()
}
}
// insert number = 2
// insert id = 7
// insert id = 8
- 查询日志数据
runBlocking {
try {
val logList = DatabaseManager.db.logDao.getLogList()
println("query number = ${logList.size}")
logList.map {
println("query = $it")
}
} catch (exception: Exception) {
println("query error = ${exception.message}")
exception.printStackTrace()
}
}
// query number = 8
// query = LogEntity(id=1, time=1621422503268, type=test, code=200, message=this is a test log)
// query = LogEntity(id=2, time=1621422505357, type=test, code=200, message=this is a test log)
// ...
// query = LogEntity(id=7, time=1621475964075, type=test, code=1, message=this is a test log)
// query = LogEntity(id=8, time=1621475964075, type=test, code=1, message=this is a test log)
通过对比RxJava配合Room的使用方法,在使用kotlin协程配合Room进行使用时还是非常简单的,首先是不需要手动切换线程了,其次是在获取返回值的时候是以函数返回值的方式获取数据,不需要传递回调对象。
RxJava在执行过程中出现异常会回调到异常处理的Consumer
中,kotlin中的异常需要使用try...cache
捕获处理。
本文章的目标是打造一个日志系统,为此我还封装了一个工具类LogUtil
import android.util.Log
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.lang.Exception
/**
* 日志工具类
*/
object LogUtil {
var DEBUG = true
var DEFAULT_TAG = "DebugLog"
/**
* 仅打印日志
*/
fun print(content: Any?) {
if (DEBUG) {
Log.d(DEFAULT_TAG, content.toString())
}
}
/**
* 输出错误日志
*/
fun error(message: String, throwable: Throwable) {
if (DEBUG) {
Log.e(DEFAULT_TAG, message, throwable)
}
}
/**
* 保存日志
* @param type 日志类型
* @param code 日志代码
* @param message 日志信息
*/
fun saveLog(type: String, code: Int, message: String) {
GlobalScope.launch {
try {
print("saveLog{$message}")
DatabaseManager.db.logDao.save(LogEntity(type, code, message))
} catch (exception: Exception) {
error("Handle Exception in LogUtil.saveLog", exception)
}
}
}
}
使用方法:
LogUtil.saveLog("test", 1, "this is a test log")
通过一些列的操作,再把刚刚的各种类放到一个单独的模块中,我们就得到了一个可以方便接入到各个项目中的日志系统。