写在前面
在前面咱们学习了 DataStore:Jetpack学习:轻松把握DataStore – ()
DataStore 更适合小型或简略的数据集,由于它不支持部分更新或参照完好性。假如咱们的需求需求完结部分更新、引证完好性或大型/杂乱数据集,此刻咱们应该考虑运用 Room。
可能有朋友会问 Room 是什么?Room 持久性库是谷歌推出的一个 Jetpack 组件,它在 SQLite 上供给了一个笼统层,以便在充分利用 SQLite 的强壮功用的一起,能够流畅地拜访数据库。
Room 的优势:
针对 SQL 查询的编译时验证。
可最大限度减少重复和简略犯错的样板代码的便利注解。
简化了数据库搬迁路径。
所以相比较直接运用 SQLite ,官方更推荐运用 Room 来处理大量结构化数据。
参阅文献
嘿嘿由于是老常识,所以这次只参阅了官方文档:
运用 Room 将数据保存到本地数据库 | Android 开发者 | Android Developers (google.cn)
预备
文本的事例是在此文事例的基础上开端的:
RecycerView-Selection:简化RecycerView列表项的挑选 – ()
假如懒得阅览能够直接拉到最后复制粘贴完好代码然后持续阅览。
由于本文是为了介绍 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列表项的挑选 – ()
写在最后
文本首要目的是让未触摸过 Room 的朋友更快上手运用,假如没有特殊需求根本也是够用的,但 Room 的功用不止于此,有爱好的朋友能够翻翻官方文档进一步学习,后续我也会尽快写进阶篇再细讲一些 Room 的常识。