前言
时间: 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 依赖
groovyimplementation("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
kotlinvar commonDataBase = Room.databaseBuilder(context = this, CommonDataBase::class.java, "part77.db").build()
可以看出,在实例化时需要用到一个内存泄漏大哥------context,并且如果每个activity都需要使用就需要创建多次。这肯定是有问题的。那怎么解决呢?
-
新建MyApplication继承Application
kotlinclass 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 实例化它
kotlincompanion 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 中调用数据库操作
kotlinif (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 改版的自己的版本,可以根据喜好自己选择
依赖引入:
groovyimplementation("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 implementation
、CommonDataBase_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下手了?好好好,这么玩是吧。去官网看看
大致方法
-
首先在Project层 build.gradle.kts 引入插件
groovyplugins { id("com.google.devtools.ksp") version "1.8.10-1.0.9" apply false }
版本( 最好 )对应Kotlin版本,版本查看 KSP(GitHub)。ps:不对应可能也不会有大问题
-
在Module层 build.gradle.kts 添加 插件引用
groovyplugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("kotlin-kapt")///kapt插件 id("com.google.devtools.ksp")///KSP插件 }
然后把kapt改成ksp
groovyimplementation("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()
kotlinval commDb: CommonDataBase by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { Room.databaseBuilder(MyApplication.appContext, CommonDataBase::class.java, "part7.db") .allowMainThreadQueries().build() }
运行就ok了。
-
不让主线程运行,我就不在主线程中运行
Activity 中添加挂载函数
kotlinprivate 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)
kotlinif (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仓库中某一个具体文件夹内容的方法
欢迎点赞评论