一、Room + ViewModel + LiveData 框架使用核心要点
1、Room 框架优化分析
在上一篇博客 【Jetpack】使用 Room 框架访问 Android 平台 SQLite 数据库 ( 导入依赖 | 定义 Entity 实体类 | 定义 Dao 数据库访问对象接口 | 定义数据库实例类 ) 中 , 实现了 使用 Room 框架访问 Android 中的 SQLite 数据库的操作 , 每当数据库中的数据发生变化时 , 就需要开启线程 , 重新获取数据库中的数据 ;
为了优化上述问题 , 可以引入 LiveData 和 ViewModel ,
- ViewModel 是 视图 View 与 数据模型 Model 之间 数据交互的 桥梁 ;
- LiveData 是基于 ViewModel 的 , 是 对 ViewModel 数据维护的一个补充 ;
在 ViewModel 中使用了 LiveData 后 ,
先调用 LiveData#observe 函数 为 LiveData 设置 androidx.lifecycle.Observer 监听器 ,
如果 该监听器 监听到了 LiveData 数据变化 ,
直接 回调 androidx.lifecycle.Observer 监听器 的 androidx.lifecycle.Observer#onChanged 函数 ,
最终在上述回调函数中执行 查询数据库 和 更新视图 操作 ;
2、Google 官方建议的 Room + ViewModel + LiveData 架构
下图是 Google 官方 提出的 Room + ViewModel + LiveData 架构设计 建议 :
下面分析上述 架构图 中的 架构分层 ;
Model 数据模型层 :
- 本地数据访问 : 使用 Room 框架 访问本地的 SQLite 数据库 ;
- 远程数据访问 : 使用 Retrofit 框架 访问 远程数据源 ( Remote Data Source ) ;
Repository 层 : 该层负责 封装 Model 数据模型层 , 用于与 ViewModel 层进行交互 ;
ViewModel 视图模型层 : 该层 不与 Room 和 Retrofit 直接交互 , 而是与 Repository 层 进行交互 ; 在 ViewModel 层引入 LiveData 监听数据变化 , 如果数据发生变化则在 LiveData 设置的 androidx.lifecycle.Observer 监听器回调中 更新 View 视图 ;
View 视图层 : Activity / Fragment 负责视图显示的 系统组件 , 负责维护 Android 视图组件 , 显示的数据由 ViewModel 提供 ;
3、Room 与 LiveData 结合使用要点
对于 Room 框架使用来说 ,
- Room 与 LiveData 结合使用 ,
- Room 单独使用 ,
唯一的区别是 Room 框架中的 Dao 数据访问接口对象 中的 查询方法 , 其返回值类型改为 LiveData 类型 , LiveData 的泛型为 原来的查询方法的返回值类型 ;
Dao 查询方法的返回值由 List<Student> 变为 LiveData<List<Student>> ;
Room 框架中 , Entity 实体类 , Database 数据库实体类 , 定义方式保持不变 ,
- Entity 实体类 使用 @Entity 注解修饰 , 并使用 @PrimaryKey 注解修饰主键 , 使用 @ColumnInfo 注解 修饰普通字段 , 使用 @Ignore 注解 修饰不需要的字段或方法 ;
- Database 数据库实体类 使用 @Database 注解修饰该类 , 其中定义 获取 Dao 数据库访问对象的抽象方法 , 以及 将该抽象类设置成 单例类 , 在单例对象初始化时创建数据库 ;
在 Room 框架中的 Dao 数据库访问对象接口 的定义方式需要作出改变 , 涉及到数据库查询的 接口方法时 , 其返回值需要 返回 LiveData 类型 , 泛型设置为 List<Student> 类型 ;
/**
* 查询数据库表
*/
@Query("select * from student")
fun query(): LiveData<List<Student>>
/**
* 根据传入的 id 查询数据库表
* 在注解中使用 :id 调用参数中的 id: Int
*/
@Query("select * from student where id = :id")
fun query(id: Int): LiveData<List<Student>>
原来的方法如下 , 其查询接口的返回值是 List<Student> ;
/**
* 查询数据库表
*/
@Query("select * from student")
fun query(): List<Student>
/**
* 根据传入的 id 查询数据库表
* 在注解中使用 :id 调用参数中的 id: Int
*/
@Query("select * from student where id = :id")
fun query(id: Int): List<Student>
Room 框架的用法 , 参考 【Jetpack】使用 Room 框架访问 Android 平台 SQLite 数据库 ( 导入依赖 | 定义 Entity 实体类 | 定义 Dao 数据库访问对象接口 | 定义数据库实例类 ) 博客 ;
4、Repository 层核心要点
Repository 层负责 封装 Model 数据模型层 , 用于与 ViewModel 层进行交互 ;
因此在 Repository 中 , 需要 持有 Dao 数据访问接口对象 ;
lateinit var dao: StudentDao
而 Dao 又是通过 Database 得到的 ,
因此在 该 Repository 中需要先获取 Database 数据库实例类对象 , 然后通过 Database 获取 Dao 数据访问接口 ;
constructor(context: Context) {
var database = StudentDatabase.inst(context)
this.dao = database.studentDao()
}
此外 , 还需要 在 Repository 层中 , 维护数据库的 增删改查 方法 , 该操作直接调用 Dao 数据库访问接口对象完成 ,
fun insert(student: Student) {
this.dao.insert(student)
}
fun query(): LiveData<List<Student>> {
return this.dao.query()
}
fun update(student: Student) {
this.dao.update(student)
}
fun delete(id: Int) {
var student = Student(id)
this.dao.delete(student)
}
特别注意 , 为了 将 Room 与 LiveData 结合 , Dao 查询方法的返回值是 LiveData 类型 ;
fun query(): LiveData<List<Student>> {
return this.dao.query()
}
5、ViewModel + Room 结合使用
根据 Google 官方的架构建议 , ViewModel 不与 Room 直接交互 , 而是由 Repository 将 Room 封装起来 , 由 ViewModel 与 Repository 进行交互 ;
ViewModel 与 Room 结合使用 , 实际上与 Repository 进行交互 ;
ViewModel 需要继承 AndroidViewModel , 并且需要在类中维护 Repository 成员变量 ,
class ViewModel: AndroidViewModel {
lateinit var repository: Repository
constructor(application: Application) : super(application) {
this.repository = Repository(application)
}
同时 , 需要 在 ViewModel 中维护 数据库 的 增删改查 的对应函数 , 通过调用 Repository 成员边来那个实现对数据库的操作 , 查询函数 的返回值是 LiveData 类型的 ;
fun insert(student: Student) {
this.dao.insert(student)
}
fun query(): LiveData<List<Student>> {
return this.dao.query()
}
fun update(student: Student) {
this.dao.update(student)
}
fun delete(id: Int) {
var student = Student(id)
this.dao.delete(student)
}
6、Activity 组件中 ViewModel 使用要点
在 Activity 组件中 , 通过调用 ViewModel 视图模型获取 数据库中的数据 , ViewModel 调用 Repository 层的增删改查方法 , Repository 调用 Room 框架的相关方法操作 SQLite 数据库 ;
首先 , 获取 ViewModel 视图模型 ;
// 获取 ViewModel 视图模型对象
var viewModel: ViewModel = ViewModelProvider(
this,
AndroidViewModelFactory(application)).get(ViewModel::class.java)
然后 , 为 ViewModel 视图模型中获取的 LiveData 数据设置 Observer 监听 ;
// 为 ViewModel 中获取的 LiveData 数据设置 Observer 监听
viewModel.query().observe(this, object: Observer<List<Student>> {
override fun onChanged(t: List<Student>?) {
Log.i("MainActivity", "Observer#onChanged 回调, List<Student>: " + t)
}
})
最后 , 通过调用 ViewModel 中定义的 数据库操作 方法 , 修改数据库中的数据 , 如果数据库中的数据发生了改变 , 就会自动回调 Observer#onChanged 方法 ;
thread(start = true) {
Thread.sleep(500)
// 插入数据
var s1 = Student("Tom", 18)
var s2 = Student("Jerry", 16)
viewModel.insert(s1)
Log.i("MainActivity", "插入数据 S1 : " + s1)
Thread.sleep(500)
viewModel.insert(s2)
Log.i("MainActivity", "插入数据 S2 : " + s2)
Thread.sleep(500)
s2 = Student(2, "Jack", 60)
viewModel.update(s2)
Log.i("MainActivity", "更新数据 S2 : " + s2)
Thread.sleep(500)
// 删除数据
viewModel.delete(1)
Log.i("MainActivity", "删除数据 id = 1")
Thread.sleep(500)
var students = viewModel.repository.dao.query()
Log.i("MainActivity", "主动查询 : LiveData : " + students + " , 实际数据 : " + students?.value)
var students2 = viewModel.repository.dao.queryList()
Log.i("MainActivity", "主动查询2 : " + students2)
}
7、Room 框架主动查询数据库数据需保留除 LiveData 返回值外的正常查询方法
Room 框架 与 LiveData 结合使用之后 , 在 Room 框架中的 Dao 数据库访问接口中 定义了 LiveData 返回值类型的查询方法 ;
/**
* 查询数据库表
*/
@Query("select * from student")
fun query(): LiveData<List<Student>>
/**
* 根据传入的 id 查询数据库表
* 在注解中使用 :id 调用参数中的 id: Int
*/
@Query("select * from student where id = :id")
fun query(id: Int): LiveData<List<Student>>
上述定义的 fun query(): LiveData<List<Student>> 查询方法 , 只能在数据库数据发生改变被动回调时才能查询出数据 , 如果主动调用该方法查询数据库 , 会返回一个空数据的 LiveData ;
如果想要手动主动查询数据库 , 需要保留非 LiveData 返回值的查询方法 , 也就是如下面的代码所示 , 同时维护两组查询方法接口 ,
- 与 LiveData 交互的接口 , 返回 LiveData<List<Student>> 类型 返回值 ;
- 手动主动调用的查询 数据库的 方法接口 , 返回 List<Student> 类型 返回值 ;
/**
* 查询数据库表
*/
@Query("select * from student")
fun query(): LiveData<List<Student>>
/**
* 查询数据库表
*/
@Query("select * from student")
fun queryList(): List<Student>
二、完整代码示例
1、build.gradle 构建脚本
在 build.gradle 构建脚本 中 , 需要配置 Kotlin 插件 和 Kotlin 注解插件 ;
plugins {
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
}
导入 Room 依赖库 , 注意这是 Kotlin 版本需要导入的依赖库 , 如果是 Java 版本 , 需要导入另外的注解处理器 ;
// 导入 Room 依赖库
implementation 'androidx.room:room-runtime:2.2.5'
// 导入注解处理器 ( Kotlin )
kapt 'androidx.room:room-compiler:2.2.5'
完整代码 :
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
}
android {
namespace 'kim.hsl.rvl'
compileSdk 32
defaultConfig {
applicationId "kim.hsl.rvl"
minSdk 21
targetSdk 32
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
viewBinding {
// 启用 ViewBinding
enabled = true
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
// 导入 Room 依赖库
implementation 'androidx.room:room-runtime:2.2.5'
// 导入注解处理器 ( Kotlin )
kapt 'androidx.room:room-compiler:2.2.5'
// 导入注解处理器 ( Java )
//annotationProcessor 'androidx.room:room-compiler:2.2.5'
}
2、Room 框架相关代码
Entity 实体类
Entity 实体类 使用 @Entity 注解修饰 , 并使用 @PrimaryKey 注解修饰主键 , 使用 @ColumnInfo 注解 修饰普通字段 , 使用 @Ignore 注解 修饰不需要的字段或方法 ;
完整代码 :
package kim.hsl.rvl
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
/**
* 定义数据库表 Entity 实体 / 同时定义数据库表 和 对鹰的实体类
* 设置该数据类对应数据库中的一张数据表, 表名为 student
* 该数据库表中的数据对应一个 Student 类实例对象
*/
@Entity(tableName = "student")
class Student {
/**
* @PrimaryKey 设置主键 autoGenerate 为自增
* @ColumnInfo name 设置列名称 / typeAffinity 设置列类型
*/
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)
var id: Int = 0
/**
* 姓名字段
* 数据库表中的列名为 name
* 数据库表中的类型为 TEXT 文本类型
*/
@ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT)
lateinit var name: String
/**
* 年龄字段
* 数据库表中的列名为 age
* 数据库表中的类型为 INTEGER 文本类型
*/
@ColumnInfo(name = "age", typeAffinity = ColumnInfo.INTEGER)
var age: Int = 0
/**
* 有些属性用于做业务逻辑
* 不需要插入到数据库中
* 使用 @Ignore 注解修饰该属性字段
*/
@Ignore
lateinit var studentInfo: String
/**
* 默认的构造方法给 Room 框架使用
*/
constructor(id: Int, name: String, age: Int) {
this.id = id
this.name = name
this.age = age
}
/**
* 使用 @Ignore 标签标注后
* Room 就不会使用该构造方法了
* 这个构造方法是给开发者使用的
*/
@Ignore
constructor(name: String, age: Int) {
this.name = name
this.age = age
}
/**
* 使用 @Ignore 标签标注后
* Room 就不会使用该构造方法了
* 这个构造方法是给开发者使用的
*/
@Ignore
constructor(id: Int) {
this.id = id
}
override fun toString(): String {
return "Student(id=$id, name='$name', age=$age)"
}
}
Dao 数据库访问接口对象
在 Room 框架中的 Dao 数据库访问对象接口 的定义方式需要作出改变 , 涉及到数据库查询的 接口方法时 , 其返回值需要 返回 LiveData 类型 , 泛型设置为 List<Student> 类型 ;
完整代码 :
package kim.hsl.rvl
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
/**
* 数据库访问对象接口 / 使用 @Dao 注解修饰
* 提供数据库的增删改查方法
*/
@Dao
interface StudentDao {
/**
* 向数据库表中插入元素
*/
@Insert
fun insert(student: Student)
/**
* 从数据库表中删除元素
*/
@Delete
fun delete(student: Student)
/**
* 修改数据库表元素
*/
@Update
fun update(student: Student)
/**
* 查询数据库表
*/
@Query("select * from student")
fun query(): LiveData<List<Student>>
/**
* 根据传入的 id 查询数据库表
* 在注解中使用 :id 调用参数中的 id: Int
*/
@Query("select * from student where id = :id")
fun query(id: Int): LiveData<List<Student>>
/**
* 查询数据库表
*/
@Query("select * from student")
fun queryList(): List<Student>
/**
* 根据传入的 id 查询数据库表
* 在注解中使用 :id 调用参数中的 id: Int
*/
@Query("select * from student where id = :id")
fun queryList(id: Int): List<Student>
}
Database 数据库实体类
Database 数据库实体类 使用 @Database 注解修饰该类 , 其中定义 获取 Dao 数据库访问对象的抽象方法 , 以及 将该抽象类设置成 单例类 , 在单例对象初始化时创建数据库 ;
完整代码 :
package kim.hsl.rvl
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [Student::class], version = 1, exportSchema = false)
abstract class StudentDatabase: RoomDatabase() {
/**
* 获取 数据库访问 对象
* 这是必须要实现的函数
*/
abstract fun studentDao(): StudentDao
companion object {
lateinit var instance: StudentDatabase
fun inst(context: Context): StudentDatabase {
if (!::instance.isInitialized) {
synchronized(StudentDatabase::class) {
// 创建数据库
instance = Room.databaseBuilder(
context.applicationContext,
StudentDatabase::class.java,
"student_database.db")
.allowMainThreadQueries() // Room 原则上不允许在主线程操作数据库
// 如果要在主线程操作数据库需要调用该函数
.build()
}
}
return instance;
}
}
}
3、Repository 代码
Repository 代码 负责 封装 Model 数据模型层 , 用于与 ViewModel 层进行交互 ;
package kim.hsl.rvl
import android.content.Context
import androidx.lifecycle.LiveData
class Repository {
lateinit var dao: StudentDao
constructor(context: Context) {
var database = StudentDatabase.inst(context)
this.dao = database.studentDao()
}
fun insert(student: Student) {
this.dao.insert(student)
}
fun query(): LiveData<List<Student>> {
return this.dao.query()
}
fun update(student: Student) {
this.dao.update(student)
}
fun delete(id: Int) {
var student = Student(id)
this.dao.delete(student)
}
}
4、ViewModel 代码
负责数据的维护 , 显示到 View 组件中 ;
完整代码 :
package kim.hsl.rvl
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
class ViewModel: AndroidViewModel {
lateinit var repository: Repository
constructor(application: Application) : super(application) {
this.repository = Repository(application)
}
fun insert(student: Student) {
this.repository.insert(student)
}
fun query(): LiveData<List<Student>> {
return this.repository.query()
}
fun update(student: Student) {
this.repository.update(student)
}
fun delete(id: Int) {
this.repository.delete(id)
}
}
5、Activity 组件中的最终调用代码
通过调用 ViewModel 视图模型 , 访问 Room 数据库框架 , 对数据进行增删改查 , 并通过 LiveData 监听数据库中的数据 ,
如果数据库中的数据发生改变 , 自动回调 LiveData 的 Observer 监听器中的 onChanged 回调方法 ;
完整代码 :
package kim.hsl.rvl
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory
import kim.hsl.rvl.databinding.ActivityMainBinding
import kotlin.concurrent.thread
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(getLayoutInflater())
setContentView(binding.root)
// 获取 ViewModel 视图模型对象
var viewModel: ViewModel = ViewModelProvider(
this,
AndroidViewModelFactory(application)).get(ViewModel::class.java)
// 为 ViewModel 中获取的 LiveData 数据设置 Observer 监听
viewModel.query().observe(this, object: Observer<List<Student>> {
override fun onChanged(t: List<Student>?) {
Log.i("MainActivity", "Observer#onChanged 回调, List<Student>: " + t)
}
})
thread(start = true) {
Thread.sleep(500)
// 插入数据
var s1 = Student("Tom", 18)
var s2 = Student("Jerry", 16)
viewModel.insert(s1)
Log.i("MainActivity", "插入数据 S1 : " + s1)
Thread.sleep(500)
viewModel.insert(s2)
Log.i("MainActivity", "插入数据 S2 : " + s2)
Thread.sleep(500)
s2 = Student(2, "Jack", 60)
viewModel.update(s2)
Log.i("MainActivity", "更新数据 S2 : " + s2)
Thread.sleep(500)
// 删除数据
viewModel.delete(1)
Log.i("MainActivity", "删除数据 id = 1")
Thread.sleep(500)
var students = viewModel.repository.dao.query()
Log.i("MainActivity", "主动查询 : LiveData : " + students + " , 实际数据 : " + students?.value)
var students2 = viewModel.repository.dao.queryList()
Log.i("MainActivity", "主动查询2 : " + students2)
}
}
}
6、执行结果
由下面的执行结果可以得出如下结论 :
- 为 ViewModel 中的数据库查询方法 获取的 LiveData , 首次设置 Observer 监听 , 会回调一次, 首次查询时 , 数据库为空 , 没有查到任何数据 , 最终得到
[]
打印结果 ;
// 为 ViewModel 中获取的 LiveData 数据设置 Observer 监听
viewModel.query().observe(this, object: Observer<List<Student>> {
override fun onChanged(t: List<Student>?) {
Log.i("MainActivity", "Observer#onChanged 回调, List<Student>: " + t)
}
})
- 第一次 插入数据 S1 , 数据库数据发生改变 , 自动触发 Observer#onChanged 回调 , 此时数据库中有数据
[Student(id=1, name='Tom', age=18)]
; - 第二次 插入数据 S2 , 数据库数据发生改变 , 自动触发 Observer#onChanged 回调 , 此时数据库中有数据
[Student(id=1, name='Tom', age=18), Student(id=2, name='Jerry', age=16)]
; - 更新数据 S2 时 , 数据库数据发生改变 , 自动触发 Observer#onChanged 回调 , 此时数据库中有数据
[Student(id=1, name='Tom', age=18), Student(id=2, name='Jack', age=60)]
, id 为 2 的数据内容发生了改变 ; - 删除 id = 1 的数据 , 数据库数据发生改变 , 自动触发 Observer#onChanged 回调 , 此时数据库中有数据
[Student(id=2, name='Jack', age=60)]
; -
调用 Dao 中返回 LiveData 的接口方法查询数据库 , 返回
androidx.room.RoomTrackingLiveData@8726677
, 但其中的数据为空 ; -
调用 Dao 中返回 List<Student> 的接口方法查询数据库 , 返回数据为
[Student(id=2, name='Jack', age=60)]
;
执行结果 :文章来源:https://www.toymoban.com/news/detail-458397.html
2023-05-24 16:49:49.225 I/MainActivity: Observer#onChanged 回调, List<Student>: []
2023-05-24 16:49:49.475 I/MainActivity: 插入数据 S1 : Student(id=0, name='Tom', age=18)
2023-05-24 16:49:49.481 I/MainActivity: Observer#onChanged 回调, List<Student>: [Student(id=1, name='Tom', age=18)]
2023-05-24 16:49:49.979 I/MainActivity: 插入数据 S2 : Student(id=0, name='Jerry', age=16)
2023-05-24 16:49:49.981 I/MainActivity: Observer#onChanged 回调, List<Student>: [Student(id=1, name='Tom', age=18), Student(id=2, name='Jerry', age=16)]
2023-05-24 16:49:50.482 I/MainActivity: 更新数据 S2 : Student(id=2, name='Jack', age=60)
2023-05-24 16:49:50.484 I/MainActivity: Observer#onChanged 回调, List<Student>: [Student(id=1, name='Tom', age=18), Student(id=2, name='Jack', age=60)]
2023-05-24 16:49:50.996 I/MainActivity: 删除数据 id = 1
2023-05-24 16:49:51.009 I/MainActivity: Observer#onChanged 回调, List<Student>: [Student(id=2, name='Jack', age=60)]
2023-05-24 16:49:51.497 I/MainActivity: 主动查询 : LiveData : androidx.room.RoomTrackingLiveData@8726677 , 实际数据 : null
2023-05-24 16:49:51.500 I/MainActivity: 主动查询2 : [Student(id=2, name='Jack', age=60)]
文章来源地址https://www.toymoban.com/news/detail-458397.html
到了这里,关于【Jetpack】Room + ViewModel + LiveData 综合使用 ( 核心要点说明 | 组合方式 | 代码示例 )的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!