写在前面

在前面咱们学习了 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)

此刻程序界面如下:

Jetpack学习:一文入门Room

运用

依靠导入

版本查看: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 从数据库中检索数据,作为相关的数据实体目标的实例。此外,运用还能够运用界说的数据实体更新相应表中的行,或许创立新行供刺进。

Jetpack学习:一文入门Room

创立实体类

首先咱们来修正咱们的实体类 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 的组件。一起设置参数entitiesversionentities表明一切与数据库相关的数据实体,而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 的常识。