Jetpack学习:一文入门Room

2,508 阅读12分钟

写在前面

在前面我们学习了 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 入门界面.png

使用

依赖导入

版本查看: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 从数据库中检索数据,作为关联的数据实体对象的实例。此外,应用还可以使用定义的数据实体更新相应表中的行,或者创建新行供插入。
Room架构示意图.png

创建实体类

首先我们来修改我们的实体类 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列表项的选择 - 掘金 (juejin.cn)

写在最后

文本主要目的是让未接触过 Room 的朋友更快上手使用,如果没有特殊需求基本也是够用的,但 Room 的功能不止于此,有兴趣的朋友可以翻翻官方文档进一步学习,后续我也会尽快写进阶篇再细讲一些 Room 的知识。