Jetpack系列(六) -- Room

前言

时间: 23/09/23

AndroidStudio版本: Giraffe 2022.3.1 JDK:17 开发语言: Kotlin

Gradle版本: 8.0 Gradle plugin Version: 8.1.1

概述

前面讲了很多组件,但都是数据控制或者视图相关的,而在实际开发过程中,往往会涉及数据存储。Android中的数据存储方式有很多,包括文件流、SharePreference、SQLite等。而数据库( SQLite )往往是运用最多最广泛的。

依据软件工程,在软件开发(代码实现)前的系统设计阶段就需要确定实体关系图,而这也关系到数据库的建表及维护甚至是整个代码的设计和逻辑。足见数据库在软件开发过程中的重要程度。

一套完整的组件当然不能少了数据库存储功能,Jetpack 也不会例外。Jetpack 中官方推荐使用的是Room数据库框架组件。Room在 SQLite 的基础上又提供了一个抽象层,充分利用SQLite强大功能的同时,还能够流畅地访问数据库。

本节就讲述一下Room的基本使用。

Room的基本使用

  • 引入 Room 依赖

    groovy 复制代码
    implementation("androidx.room:room-runtime:2.5.2")
    annotationProcessor("androidx.room:room-compiler:2.5.2")

    参照书本中的依赖添加,这个依赖是存在问题的,后面有解决办法。

  • 新建一个data类作为实体bean,并添加@Entity标记

    kotlin 复制代码
    @Entity(tableName="User")
    class User(
        @PrimaryKey(autoGenerate = true) var id: Int?,//主键,自增为true
        @ColumnInfo(name = "username") var username: String,
        var password: String
    ) {
        override fun toString(): String {
            return "[ username=$username, password=$password ]"
        }
    }

    系统会使用@Entity标记的实体类创建对应的数据表,同时至少设置一个主键,使用@PrimaryKey注解来设置属性为主键。

    一般来说,在数据库建表过程中,表名与类名一致,字段名与类属性名一致,当然也可以使用其它名称来规定表名或字段名。表名使用@Entity(tableName="User"),即在Entity注解后添加tableName并设置想要的表名。字段名则是在属性前添加@ColumnInfo(name = "username")注解并设置name。

  • 新建UserDao接口类,并添加@Dao注释

    kotlin 复制代码
    @Dao
    interface UserDao {
        @Insert(entity = User::class)
        fun register(user: User)
    
        @Query("Select * from User where username == :username and password == :password")
        fun login(username: String, password: String): User?
    }

    register是注册用户,对应数据库的插入操作( 增 )

    login是用户登录,对应数据库的查找操作( 查 )

  • 新建抽象类 CommonDataBase 继承自 RoomDataBase,并添加 @Database 注解

    kotlin 复制代码
    @Database(version = 1, entities = [User::class])
    abstract class CommonDataBase : RoomDatabase() {
        abstract val userDao: UserDao
    }

    在 @Database 注解中有两个参数,version 是数据库版本,entities 是实体类,这是个 Array 类型,因为在实际开发过程中,一个数据库中往往有很多数据表,一般情况下一个数据表就对应一个实体类。这里我们暂时只添加 User 类。

    添加一个抽象变量userDao,类型为UserDao,后续可以直接调用UserDao中的数据库操作。

  • 实例化DataBase

    kotlin 复制代码
    var commonDataBase =
                Room.databaseBuilder(context = this, CommonDataBase::class.java, "part77.db").build()

    可以看出,在实例化时需要用到一个内存泄漏大哥------context,并且如果每个activity都需要使用就需要创建多次。这肯定是有问题的。那怎么解决呢?

  • 新建MyApplication继承Application

    kotlin 复制代码
    class MyApplication : Application() {
        private val TAG = "MyApplication"
    
        companion object {
            @SuppressLint("StaticFieldLeak")
            lateinit var appContext: Context
        }
    
        override fun onCreate() {
            super.onCreate()
            appContext = applicationContext
            var commonDataBase =
                Room.databaseBuilder(context = this, 
                	CommonDataBase::class.java, "part77.db").build()
            Log.d(TAG, 
    			"DataBase file path: ${appContext.getDatabasePath("part7.db").absolutePath}")
        }
    }

    总所周知,application也有context,但是整个程序就只有一个 applicationContext,那么我们就可以把commonDataBase放在application中来实例化,这样既不会重复创建也不用担心引用 Activity 的context导致内存泄漏。但是这样写还是存在一个问题,那就是在 Activity 中不好调用。

    可以参考我 MyApplication 中 appContext的写法来实现外部类调用,也可以用下面这个方法(推荐)

  • 在CommonDataBase 类中添加静态变量commDb,并通过 lazy 实例化它

    kotlin 复制代码
        companion object {
            val commDb: CommonDataBase by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
                Room.databaseBuilder(MyApplication.appContext, 
                                     CommonDataBase::class.java, "part7.db").build()
            }
        }

    从 mode 中可以看出,这种方式是线程安全模式,所以推荐使用这个方式,至于引用的context,当然还是调用application中的conext。

    记得注释application中的实例化操作。

  • 编写界面

    xml 复制代码
    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">
    
        <data>
    
            <variable
                name="user"
                type="com.may.part_7.entity.User" />
        </data>
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:paddingStart="20dp"
            android:paddingEnd="20dp"
            tools:context=".activities.LoginActivity">
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:text="@string/login"
                android:textSize="28sp"
                android:layout_marginTop="10dp"
                android:textStyle="bold" />
    
            <com.google.android.material.textfield.TextInputLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="150dp">
    
                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/et_name"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:hint="@string/input_your_username"
                    android:text="@{user.username}"
                    android:textSize="18sp" />
            </com.google.android.material.textfield.TextInputLayout>
    
            <com.google.android.material.textfield.TextInputLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="20dp">
    
                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/et_pass"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:hint="@string/input_your_password"
                    android:inputType="textPassword"
                    android:text="@{user.password}"
                    android:textSize="18sp" />
            </com.google.android.material.textfield.TextInputLayout>
    
            <Button
                android:id="@+id/login"
                android:layout_width="match_parent"
                android:layout_height="58dp"
                android:layout_marginStart="4dp"
                android:layout_marginTop="40dp"
                android:layout_marginEnd="4dp"
                android:text="@string/login"
                android:textSize="16sp" />
    
            <TextView
                android:id="@+id/register"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="end"
                android:layout_marginTop="10dp"
                android:layout_marginEnd="16dp"
                android:text="@string/register"
                android:textColor="@color/blue_register"
                android:textSize="14sp" />
    
        </LinearLayout>
    </layout>

    比较多,就贴一个login界面的代码吧。 demo代码文末有。

  • Activity 中调用数据库操作

    kotlin 复制代码
            if (name.isEmpty() || pass.isEmpty()) {
                Toast.warning(appContext, " Please input username or password ")
            } else if (CommonDataBase.commDb.userDao.login(name, pass) != null) {
                Toast.success(appContext, " Welcome! $name ")
                startActivity(Intent(this@LoginActivity, MainActivity::class.java))
            } else {
                Toast.error(appContext, " Not Found ")
            }

    很简单也很好理解,判断是否为空,判断数据库拿到的数据是否为null,并作出不同的响应。

    其它具体代码就不贴了

    这里的Toast是我根据开源项目 Toasty 改版的自己的版本,可以根据喜好自己选择

依赖引入:

groovy 复制代码
implementation("com.github.Beacon0423.myLib:toast:v1.1.2")

遇到的问题1

arduino 复制代码
java.lang.RuntimeException: Cannot find implementation for com.may.part_7.database.CommonDataBase. CommonDataBase_Impl does not exist

执行到数据库操作时,App非常优雅地闪退了。看的脑瓜子嗡嗡的。看输出日志,显示 Cannot find implementationCommonDataBase_Impl does not exist,经典看不懂,我什么时候搞了个CommonDataBase_Impl 这玩意了?我不是写的CommonDataBase吗?

个人经验,一般来说,代码编写过程中报 Runtime 错,并且出现这种似是非是的东西,相信自己不是代码写的有问题,是依赖引入的问题。

于是我去问了一下ChatGPT先生,GPT先生说不太清楚,让我看看依赖引入有没有问题。于是我只能求助StackOverflow,里头有人告诉我,你用kotlin写Room,就别用 annotationProcessor("") 来引入room-compiler依赖了,用kapt。作为一个很听劝的人,我当即准备试试。

修改依赖

groovy 复制代码
implementation("androidx.room:room-runtime:2.5.2")
kapt("androidx.room:room-compiler:2.5.2")

kapt报红?忘记添加kapt插件了。文件最上添加 id("kotlin-kapt")

groovy 复制代码
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("kotlin-kapt")//this
}

大功告成 Gradle sync !!。

欸,怎么黄色警告⚠?

Replace usage of kapt with KSP?Google又对kapt下手了?好好好,这么玩是吧。去官网看看

kapt迁移到KSP

大致方法

  • 首先在Project层 build.gradle.kts 引入插件

    groovy 复制代码
    plugins {
        id("com.google.devtools.ksp") version "1.8.10-1.0.9" apply false
    }

    版本( 最好 )对应Kotlin版本,版本查看 KSP(GitHub)。ps:不对应可能也不会有大问题

  • 在Module层 build.gradle.kts 添加 插件引用

    groovy 复制代码
    plugins {
        id("com.android.application")
        id("org.jetbrains.kotlin.android")
        id("kotlin-kapt")///kapt插件
        id("com.google.devtools.ksp")///KSP插件
    }

    然后把kapt改成ksp

    groovy 复制代码
    implementation("androidx.room:room-runtime:2.5.2")
    ksp("androidx.room:room-compiler:2.5.2")

    sync now! OK, 解决了。

官网还提到了删除 kapt 的引用,但是如果你用到了 DataBinding,那就不能删掉kapt的引用( 官网原话,可不是我胡诌 )。很奇怪,可能是KSP还没完全成熟来替代kapt。当然你也可以不用KSP,也能很完美地运行

运行代码,继续刚刚的数据库操作。欸?怎么又又又闪退?

遇到的问题2

csharp 复制代码
Cannot access database on the main thread since it may potentially lock the UI for a long period of time.

这次报错就是字面意思了哈,database 是耗时操作,不允许在 UI ( 主 )线程中直接操作。

哦,这样啊。怎么办?有两种解决方法

  • 不让主线程操作?我把你设置成可以主线程中操作,修改commDb的实例化方法,添加allowMainThreadQueries()

    kotlin 复制代码
    val commDb: CommonDataBase by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
    	Room.databaseBuilder(MyApplication.appContext, 
    		CommonDataBase::class.java, "part7.db")
            .allowMainThreadQueries().build()
    }

    运行就ok了。

  • 不让主线程运行,我就不在主线程中运行

    Activity 中添加挂载函数

    kotlin 复制代码
      private suspend fun login(name: String, pass: String): User? {
          return withContext(Dispatchers.IO) {
              return@withContext CommonDataBase.commDb.userDao.login(name, pass)
          }
      }

    协程调用CoroutineScope(Job()).launch(Dispatchers.Main)

    kotlin 复制代码
            if (name.isEmpty() || pass.isEmpty()) {
                Toast.warning(appContext, " Please input username or password ")
            } else {
                CoroutineScope(Job()).launch(Dispatchers.Main) {
                    val userResult = login(name, pass)
                    if (userResult != null) {
                        Toast.success(appContext, " Welcome! $name ")
                        startActivity(Intent(this@LoginActivity, MainActivity::class.java))
                    } else {
                        Toast.error(appContext, " Not Found ")
                    }
                }
            }

    记得launch后添加Dispatchers.Main,不然不能执行主线程操作。

增删改查

一般来讲,数据库都涉及增删改查 CRUD ,但是这些其实都是基础了,也不是Room框架的重点,所以我这里就只列出了增和查。

至于删和改,我贴一下在Dao中的注释吧,其余的调用操作其实都一样,就不多说了,可以自行实现。

kotlin 复制代码
    @Update
    fun update(user: User)
    
    @Delete
    fun delete(user: User)

@Update、@Delete包括上面的@Insert都是Room封装好的。如果你需要做一些不一样的操作,可以用@Query注解,然后添加自己需要的SQL语句即可

总结

本节讲述了Room的基本用法,以及在使用过程中遇到的问题。由于本节主要讲述的是Room,所以有一些具体实现的代码并没有细说,感兴趣的可以直接下载demo运行试试。

本篇中还提到了挂载函数suspend fun,这是kotlin协程相关的运用。所以下一节就讲述一下kotlin协程吧。

demo地址(GitHub)

有私信告诉我说 demo 可以看,但是不能只下载一个文件夹,所以我这里贴一个下载GitHub仓库中某一个具体文件夹内容的方法

欢迎点赞评论

相关推荐
alexhilton8 小时前
SnapshotFlow还是collectAsState?对于Jetpack Compose来说哪个更香?
android·kotlin·android jetpack
张可9 小时前
一个KMP/CMP项目的组织结构和集成方式
android·前端·kotlin
悠哉清闲12 小时前
Android Studio C++/JNI/Kotlin 示例 三
c++·kotlin·android studio
Kiri霧18 小时前
细谈kotlin中缀表达式
开发语言·微信·kotlin
_一条咸鱼_19 小时前
Android Runtime安全上下文管理(76)
android·面试·android jetpack
_一条咸鱼_19 小时前
Android Runtime跨进程调用优化方案深度解析(75)
android·面试·android jetpack
Kapaseker20 小时前
tryCatch还是runCatch,这是一个问题
kotlin
_一条咸鱼_20 小时前
OpenGL ES 深度剖析
android·面试·android jetpack
_一条咸鱼_3 天前
Android Runtime直接内存管理原理深度剖析(73)
android·面试·android jetpack
Wy. Lsy3 天前
Kotlin基础学习记录
开发语言·学习·kotlin