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仓库中某一个具体文件夹内容的方法

欢迎点赞评论

相关推荐
alexhilton2 小时前
借助RemoteCompose开发动态化页面
android·kotlin·android jetpack
QING61817 小时前
Jetpack Compose Brush API 简单使用实战 —— 新手指南
android·kotlin·android jetpack
QING61818 小时前
Jetpack Compose Brush API 详解 —— 新手指南
android·kotlin·android jetpack
鹿里噜哩18 小时前
Spring Authorization Server 打造认证中心(二)自定义数据库表
spring boot·后端·kotlin
用户69371750013841 天前
23.Kotlin 继承:继承的细节:覆盖方法与属性
android·后端·kotlin
Haha_bj1 天前
五、Kotlin——条件控制、循环控制
android·kotlin
Kapaseker1 天前
不卖课,纯干货!Android分层你知多少?
android·kotlin
urkay-2 天前
Android 切换应用语言
android·java·kotlin·iphone·androidx
杀死那个蝈坦2 天前
监听 Canal
java·前端·eclipse·kotlin·bootstrap·html·lua
Yang-Never2 天前
Open GL ES->EGL渲染环境、数据、引擎、线程的创建
android·java·开发语言·kotlin·android studio