写在前面
在前面我们学习了 DataStore:Jetpack学习:轻松掌握DataStore - 掘金 (juejin.cn)
DataStore 更适合小型或简单的数据集,因为它不支持部分更新或参照完整性。如果我们的需求需要实现部分更新、引用完整性或大型/复杂数据集,此时我们应该考虑使用 Room。
可能有朋友会问 Room 是什么?Room 持久性库是谷歌推出的一个 Jetpack 组件,它在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。
Room 的优势:
针对 SQL 查询的编译时验证。
可最大限度减少重复和容易出错的样板代码的方便注解。
简化了数据库迁移路径。
所以相比较直接使用 SQLite ,官方更推荐使用 Room 来处理大量结构化数据。
参考文献
嘿嘿因为是老知识,所以这次只参考了官方文档:
使用 Room 将数据保存到本地数据库 | Android 开发者 | Android Developers (google.cn)
准备
文本的案例是在此文案例的基础上开始的:
RecycerView-Selection:简化RecycerView列表项的选择 - 掘金 (juejin.cn)
如果懒得阅读可以直接拉到最后复制粘贴完整代码然后继续阅读。
因为本文是为了介绍 Room 的便捷操作,所以我们加入一些按钮用于数据库的操作。
修改 activity 的 xml,增加两个 FloatingActionButton,一个用于插入数据,一个用于批量删除数据
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_telephone"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:layout_marginBottom="12sp"
android:contentDescription="delete"
android:src="@drawable/ic_baseline_delete_forever_24"
app:background="@android:color/holo_red_dark"
app:backgroundTint="@android:color/holo_red_dark"
app:layout_constraintBottom_toTopOf="@+id/fab_add"
app:layout_constraintEnd_toEndOf="parent"
app:tint="@color/white" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="24dp"
android:contentDescription="add"
android:src="@drawable/ic_baseline_exposure_plus_1_24"
app:background="@color/black"
app:backgroundTint="@color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:tint="@color/white" />
</androidx.constraintlayout.widget.ConstraintLayout>
然后回到 activity 初始化两个新增的按钮
val recyclerView: RecyclerView = findViewById(R.id.rv_telephone)
val add:FloatingActionButton = findViewById(R.id.fab_add)
val delete:FloatingActionButton = findViewById(R.id.fab_delete)
此时程序界面如下:
使用
依赖导入
版本查看:Room | Android 开发者 | Android Developers (google.cn)
def room_version = "2.5.0"
implementation("androidx.room:room-runtime:$room_version")
// 下面三选一:
// 正常导这个
annotationProcessor("androidx.room:room-compiler:$room_version")
// 有用 kapt 导入这个
kapt("androidx.room:room-compiler:$room_version")
// 有用 ksp 导入这个
ksp("androidx.room:room-compiler:$room_version")
还有 Room 和 其他控件相结合的依赖,可以按需求导入
简介
Room 包含三个主要组件:
- 数据实体 Entity:用于表示应用数据库中的表
- 数据访问对象 Dao:提供可用于查询、更新、插入和删除数据库中的数据的方法
- 数据库类 Database:用于保存数据库并作为应用持久性数据底层连接的主要访问点
数据库类为应用提供与该数据库关联的 DAO 的实例。反过来,应用可以使用 DAO 从数据库中检索数据,作为关联的数据实体对象的实例。此外,应用还可以使用定义的数据实体更新相应表中的行,或者创建新行供插入。
创建实体类
首先我们来修改我们的实体类 Person,我们之前的 Person 只是个简单的数据类,还未与 Room 挂钩,现在我们设置注解@Entity,这样就告诉 Room 我们这个数据类是数据库的实体类。同时还可以在里面设置一些参数,比较常用的是tableName,就是指定你的表名称为什么,不然表名默认为实体类的名称。
@Parcelize
@Entity(tableName = "personTable")
data class Person(
val id: Long,
val name: String,
val telephone: String,
) : Parcelable
定义主键
最简单最基础的主键定义是在对应字段前面添加注解@PrimaryKey。
其中可选添加参数autoGenerate,其值默认为 false,如果设置为 true 且主键类型为 Long 或 Int,每次插入数据会自动从1开始自增。
@PrimaryKey(autoGenerate = true) val id: Long
如果想要定义复合主键,这时候回到我们的@Entity注解,里面有个参数可以用于设置我们的复合主键
@Entity(primaryKeys = ["name", "telephone"])
忽略字段
默认情况下,Room 会为实体中定义的每个字段创建一个列。 如果某个实体中有不想保留的字段,则可以使用 @Ignore 为这些字段添加注解
@Parcelize
@Entity(tableName = "personTable")
data class Person(
@PrimaryKey(autoGenerate = true) val id: Long,
val name: String,
val telephone: String,
@Ignore val nickname: String
) : Parcelable
如果实体继承了父实体的字段,使用 @Entity 注解的 ignoredColumns 属性通常会更容易些
open class Friend {
val nickname: String? = null
}
@Parcelize
@Entity(tableName = "personTable", ignoredColumns = ["nickname"])
data class Person(
@PrimaryKey(autoGenerate = true) val id: Long,
val name: String,
val telephone: String,
) : Parcelable, Friend()
修改数据类
经过上面的介绍,我们可以开始修改我们的数据类。因为后续的数据插入逻辑是在固定数据源中随机选取,然后再添加至数据库。但由于之前的数据 id 值被写死,如果直接随机选取插入会造成主键重复,所以我们需要将主键的autoGenerate属性设为true,这样就不需要为 id 赋值,可以将其设为可空属性。
在这里出现了一个新的注解@ColumnInfo,在实体类中,每个字段通常代表表中的一列,而此注解则用于自定义与字段所关联的列属性。其中比较常用的属性为name,用于指定字段所属列的名称为什么,不然列名默认为字段名称。
@Parcelize
@Entity(tableName = "personTable")
data class Person(
@PrimaryKey(autoGenerate = true) val id: Long? = null,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "telephone") val telephone: String,
) : Parcelable
修改数据源
由于我们的数据类发生了一定的变动,原本写死的数据需要进行修改,将所有写定的 id 更改为 null 值。同时我们使用mutableListOf便于后续插入数据可以从list中随机选择。
val list: MutableList<Person> = ArrayList(
mutableListOf(
Person(null, "Mike", "86100100"),
Person(null, "Jane", "86100101"),
Person(null, "John", "86100102"),
Person(null, "Amy", "86100103"),
)
)
创建 Dao
创建完实体类,现在可以来创建我们的数据访问对象 DAO,DAO 不具有属性,但它们定义了一个或多个方法,可用于与应用数据库中的数据进行交互,如增删改查。
每个 DAO 都被定义为一个接口或一个抽象类。但对于基本用例,通常将其定义为一个接口。像实体类一样需要添加@Entity注解,DAO 需要添加注解@Dao
便捷注解
Room 提供了方便的注解,使得无需编写 SQL 语句即可实现简单插入、更新和删除的方法:
@Insert:如果@Insert方法接收单个参数,则会返回long值,该值为插入项的新rowId。如果参数是数组或集合,则应改为返回由long值组成的数组或集合。@Update:选择性地返回int值,该值指示成功更新的行数@Delete:选择性地返回int值,该值指示成功删除的行数
Room 使用主键将传递的实体实例与数据库中的行进行匹配。如果没有具有相同主键的行,Room 不会进行任何更改。
我们现在只要在函数上添加上对应的注解,我们这个功能就算完成了,是不是很简单很快速!
@Dao
interface PersonDao {
@Insert
fun insertPeople(vararg people : Person)
@Update
fun updatePeople(vararg people : Person)
@Delete
fun deletePeople(vararg people : Person)
}
查询方法
Room 提供了 @Query注解用于从数据库查询数据,或者执行更复杂的插入、更新和删除操作。
由于@Query注解需要编写 SQL 语句并将其作为 DAO 方法公开,所以 Room 会在编译时验证 SQL 查询,一旦查询出现问题,则会出现编译错误,而不是运行时失败。
由于更多涉及了 SQL 语句的知识点,所以这里不过多赘述,简单实现两个比较常用的功能以供参考学习。
@Dao
interface PersonDao {
……
// 清空数据库所有数据
@Query("DELETE FROM personTable")
fun deleteAllPeople()
// 根据 id 按照降序返回所有数据
@Query("SELECT * FROM personTable ORDER BY id DESC")
fun getAllPeople(): List<Person>
}
创建数据库
像前面两步一样,我们也要添加一个注解@Database用于表示其为我们 Room 的组件。同时设置参数entities和version。entities表示所有与数据库关联的数据实体,而version表示当前数据库的版本号。我们的数据库类必须为一个抽象类,并继承RoomDatabase。 对于与数据库关联的每个 DAO 类,数据库类必须定义一个具有零参数的抽象方法,并返回 DAO 类的实例。
所以我们最终的基础数据库类代码如下:
@Database(entities = [Person::class], version = 1)
abstract class MyDataBase : RoomDatabase(){
abstract fun personDao(): PersonDao
}
我们使用Room.databaseBuilder来创建数据库实例,它需要三个参数
- Context:数据库的 context,我们通常用 Application context
- klass:继承 RoomDatabase 并添加
@Database注解的抽象类 - name:数据库的名称 所以我们的数据库实例如下:
val database = Room.databaseBuilder(
context.applicationContext,
MyDataBase::class.java,
"my_database"
).build()
在本案例中,应用只在单个进程中运行,由于每个 RoomDatabase 实例的成本相当高,所以在实例化 MyDatabase 对象时应遵循单例设计模式。
@Database(entities = [Person::class], version = 1)
abstract class MyDataBase : RoomDatabase(){
abstract fun personDao(): PersonDao
companion object {
@Volatile
private var INSTANCE: MyDataBase? = null
fun getInstance(context: Context): MyDataBase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
MyDataBase::class.java,
"my_database"
).build()
INSTANCE = instance
instance
}
}
}
}
如果想要应用在多个进程中运行,只需要在数据库实例化时添加enableMultiInstanceInvalidation()。这样当我们在每个进程中创建 MyDatabase实例时,如果在一个进程中使共享数据库文件失效,这种失效就会自动传播到其他进程中的 MyDatabase 实例。
val database = Room.databaseBuilder(
context.applicationContext,
MyDataBase::class.java,
"my_database"
)
.enableMultiInstanceInvalidation()
.build()
增删改查
在完成按钮点击事件的具体事项前,我们要在 activity 内初始化我们的数据库。
我们通常都希望尽快使用我们的数据库,避免初始化等待时间,所以在自定义 Application 内就初始化我们的数据库
class MainApplication : Application() {
val database: MyDataBase by lazy { MyDataBase.getInstance(this) }
}
在 Activity 顶部获取我们初始化的数据库对象,与此同时使用 MyDataBase 中的抽象方法获取 DAO 的实例
private val dataBase: MyDataBase by lazy { (application as MainApplication).database }
private val personDao: PersonDao by lazy { dataBase.personDao() }
现在可以使用 DAO 实例中的方法与数据库进行交互,所以可以开始实现我们的按钮功能。
添加数据功能: 我们使用已经初始化完毕的 personDao ,调用先前定义好的insertPeople函数,从 list 列表随机获取数据插入到我们的数据库中,后刷新 adapter,就可以看到新的数据插入成功并显示。
// 随机添加数据
add.setOnClickListener {
// 随机获取数据源中的任意一个数据
list.shuffled().take(1).forEach{
// 插入数据
personDao.insertPeople(it)
}
// 更新适配器的数据列表
adapter.list = personDao.getAllPeople()
// 刷新页面
adapter.notifyItemInserted(0)
}
删除数据功能: 我们使用已经初始化完毕的 personDao ,调用先前定义好的deletePeople函数,
// 删除选中数据
delete.setOnClickListener {
// 随机获取数据源中的任意一个数据
for (item in tracker?.selection!!) {
// 删除数据
personDao.deletePeople(item)
}
// 更新适配器的数据列表
adapter.list = personDao.getAllPeople()
// 本文刷新数据不是重点,所以不考虑性能直接刷新所有数据
adapter.notifyDataSetChanged()
}
但如果现在运行程序,就会发现报错如下:
java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
这是因为 Room 不允许在主线程上访问数据库,从而避免数据库操作长时间锁定UI,这意味着我们需要将DAO 查询设为异步。在 Kotlin 中,我们通常使用协程和 Flow 来实现 DAO 的异步操作,但本文只是 Room 文章,更多的介绍放到进阶篇讲解。而在此时要解决这个问题也很简单,只要在构造数据库实例时添加allowMainThreadQueries() 强制数据库运行在主线程上运行
val instance = Room.databaseBuilder(
context.applicationContext,
MyDataBase::class.java,
"my_database"
)
.allowMainThreadQueries()
.build()
完整代码
Entity:
@Parcelize
@Entity(tableName = "personTable")
data class Person(
@PrimaryKey(autoGenerate = true) val id: Long? = null,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "telephone") val telephone: String,
) : Parcelable
DAO:
@Dao
interface PersonDao {
@Insert
fun insertPeople(vararg people : Person)
@Update
fun updatePeople(vararg people : Person)
@Delete
fun deletePeople(vararg people : Person)
@Query("DELETE FROM personTable")
fun deleteAllPeople()
@Query("SELECT * FROM personTable ORDER BY id DESC")
fun getAllPeople(): List<Person>
}
Database:
@Database(entities = [Person::class], version = 1)
abstract class MyDataBase : RoomDatabase() {
abstract fun personDao(): PersonDao
companion object {
@Volatile
private var INSTANCE: MyDataBase? = null
fun getInstance(context: Context): MyDataBase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
MyDataBase::class.java,
"my_database"
)
.allowMainThreadQueries()
.build()
INSTANCE = instance
instance
}
}
}
}
Application:
class MainApplication : Application() {
val database: MyDataBase by lazy { MyDataBase.getInstance(this) }
}
Activity:
class MainActivity : AppCompatActivity() {
private var tracker: SelectionTracker<Person>? = null
private val dataBase: MyDataBase by lazy { (application as MainApplication).database }
private val personDao: PersonDao by lazy { dataBase.personDao() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 数据源
val list: MutableList<Person> = ArrayList(
mutableListOf(
Person(null, "Mike", "86100100"),
Person(null, "Jane", "86100101"),
Person(null, "John", "86100102"),
Person(null, "Amy", "86100103"),
)
)
val recyclerView: RecyclerView = findViewById(R.id.rv_telephone)
val add: FloatingActionButton = findViewById(R.id.fab_rvs)
val delete: FloatingActionButton = findViewById(R.id.fab_delete_rvs)
// 设置 RecyclerView
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.setHasFixedSize(true)
val adapter = RVSAdapter(personDao.getAllPeople())
adapter.setHasStableIds(true)
recyclerView.adapter = adapter
// 随机添加数据
add.setOnClickListener {
list.shuffled().take(1).forEach {
personDao.insertPeople(it)
}
adapter.list = personDao.getAllPeople()
adapter.notifyItemInserted(0)
}
// 删除选中数据
delete.setOnClickListener {
for (item in tracker?.selection!!) {
personDao.deletePeople(item)
}
adapter.list = personDao.getAllPeople()
adapter.notifyDataSetChanged()
}
// 实例化 tracker
tracker = SelectionTracker.Builder(
"mySelection-1",
recyclerView,
RVSAdapter.MyKeyProvider(adapter),
RVSAdapter.MyItemDetailsLookup(recyclerView),
StorageStrategy.createParcelableStorage(Person::class.java)
).withSelectionPredicate(
SelectionPredicates.createSelectAnything()
).build()
adapter.tracker = tracker
// 监听数据
tracker?.addObserver(
object : SelectionTracker.SelectionObserver<Person>() {
override fun onSelectionChanged() {
super.onSelectionChanged()
for (item in tracker?.selection!!) {
println(item)
}
}
})
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
tracker?.onRestoreInstanceState(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
tracker?.onSaveInstanceState(outState)
}
}
布局和适配器的完整代码参考文章:
RecycerView-Selection:简化RecycerView列表项的选择 - 掘金 (juejin.cn)
写在最后
文本主要目的是让未接触过 Room 的朋友更快上手使用,如果没有特殊需求基本也是够用的,但 Room 的功能不止于此,有兴趣的朋友可以翻翻官方文档进一步学习,后续我也会尽快写进阶篇再细讲一些 Room 的知识。