在kotlin中优雅的使用Room

在之前的文章中我介绍了使用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<Long>

    @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<LogEntity>

    suspend fun getLogList(startTime: Long = 0, endTime: Long = 0): List<LogEntity> {
        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的数据库对象,之后需要调用数据库的时候直接使用这个对象即可。

后面的CreatedCallBackMigration1也是使用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")

通过一些列的操作,再把刚刚的各种类放到一个单独的模块中,我们就得到了一个可以方便接入到各个项目中的日志系统。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,271评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,725评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,252评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,634评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,549评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,985评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,471评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,128评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,257评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,233评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,235评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,940评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,528评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,623评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,858评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,245评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,790评论 2 339

推荐阅读更多精彩内容