首页 > 技术文章 > 《Android 编程权威指南》学习笔记 : 第11章 数据库与 Room 库

easy5weikai 2022-05-29 08:59 原文

第11章 数据库与 Room 库

资料

使用 Room 保留数据

引 Room 库

要使用 Room,首先添加项目依赖

  • room-runime
  • room-complier

在 app/build.gradle 中添加

apply plugin: 'kotlin-kapt' // AS插件:Kotlin注解处理器,
...
dependencies {
    ...
    //Room
    implementation 'androidx.room:room-runtime:2.4.2' // Room API:包含创建数据库的需要的类和注解
    kapt 'androidx.room:room-compiler:2.4.2'  //Room编译器,基于注解自动生成代码
}

添加依赖后,记得 sync now

定义实体

代码清单:com.example.criminalintent/Crime.kt

@Entity
data class  Crime(
    @PrimaryKey val id:UUID = UUID.randomUUID(),
    var title: String = "",
    var date: Date = Date(),
    var isSolved: Boolean = false
)
  • @Entity:表明此类是数据库实体类
  • @PrimaryKey:表明主键

创建数据库类

代码清单:app/src/main/java/com.example.criminalintent/database/CrimeDatabase.kt

@Database(entities = [ Crime::class], version = 1)
@TypeConverters(CrimeTypeConverters::class)
abstract class CrimeDatabase : RoomDatabase() {
    abstract fun crimeDao(): CrimeDao
}
  • 继承 RoomDatabase(),是抽象类
  • @Database(entities = [ Crime::class], version = 1):表明实体的集合、数据库版本
  • @TypeConverters(CrimeTypeConverters::class) 表明提供的类型转换器

创建类型转换器

Room 能直接在后台SQLite数据库表里存储基本数据类型,但是其它类型,比如:UUID,Date类型是无法识别的,这时就需要类型转换器
代码清单:src/app/main/java/com.example.criminalintent/database/CrimeTypeConverters.kt

class CrimeTypeConverters {
    @TypeConverter
    fun fromDate(date: Date?): Long? {
        return date?.time
    }

    @TypeConverter
    fun toDate(millisSinceEpoch: Long?): Date? {
        return millisSinceEpoch?.let {
            Date(it)
        }
    }

    @TypeConverter
    fun fromUUID(uuid: UUID?): String? {
        return uuid?.toString()
    }

    @TypeConverter
    fun toUUID(uuid: String?): UUID? {
        return UUID.fromString(uuid)
    }
}
  • 记得添加注释: @TypeConverter

创建数据访问对象(DAO)

代码清单:src/app/main/java/com.example.criminalintent/database/CrimeDao.kt

定义数据访问对象一个接口

@Dao
interface CrimeDao {
    @Query("select * from crime")
    fun getCrimes(): List<Crime>

    @Query("select * from crime where id=(:id)")
    fun getCrime(id: UUID): Crime?
}
  • @Query(): 注释SQL语句,Room工具会自动生成 Kotlin 代码

仓储(单例)

推荐使用仓储模式访问数据
代码清单:src/app/main/java/com.example.criminalintent/CrimeRepository.kt

class CrimeRepository private constructor(context: Context) {

    private val database: CrimeDatabase = Room.databaseBuilder(
        context.applicationContext,
        CrimeDatabase::class.java,
        DATABASE_NAME
    ).build()

    private val crimeDao = database.crimeDao()

    fun getCrimes(): List<Crime> = crimeDao.getCrimes()
    fun getCrime(id: UUID): Crime? = crimeDao.getCrime(id)

    companion object {
        private var INSTANCE: CrimeRepository? = null

        fun initialize(context: Context) {
            if(INSTANCE == null) {
                INSTANCE = CrimeRepository(context)
            }
        }

        fun get(): CrimeRepository {
            return INSTANCE ?:
              throw IllegalStateException("CrimeRepository must be initialize")
        }
    }
}

  • 使用 Room.databaseBuilder(...,...,...,).build() 创建抽象类 CrimeDatabase::class.java 的具体实现类 val database: CrimeDatabase
  • 仓储再定义引用 private val crimeDao = database.crimeDao()

创建 Appication 类

定义 CriminalIntentApplication:Application() 应用程序类,在 onCreate()中初始化 仓储
代码清单:app/src/main/java/com.example.criminalintent/CrimeRepository.kt

class CriminalIntentApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        CrimeRepository.initialize(this)
    }
}

登记应用程序类

在 AndroidManifest.xml 登记登记应用程序类 Appication 类
代码清单:app/src/main/AndroidManifest.xml

    <application
        android:name=".CriminalIntentApplication"
        ...
    />

ViewModel 中修改数据访问

代码清单:src/app/main/java/com.example.criminalintent/CrimeListViewModel.kt

class CrimeListViewModel : ViewModel() {
//    val crimes = mutableListOf<Crime>()
//    init {
//        for (i in 0 until 100){
//            val crime = Crime()
//            crime.title = "Crime  #$i"
//            crime.isSolved = i%2 == 0
//            crimes += crime
//        }
//    }

    private val crimeRepository: CrimeRepository = CrimeRepository.get()
    var crimes = crimeRepository.getCrimes()

}

上传已存在的数据库

为了测试,上传已经有数据的数据库表,运行模拟器后,在其 【Device File Exploer】中 找到**data/date**目录,

找到应用程序目录 data/date/com.example.criminalintent,右键菜单选择【Upload...】菜单项,在本电脑中找到随书示例代码章节中找到数据库文件,点击上传

本书随书资料下载:https://www.ituring.com.cn/book/2771

运行崩溃

运行模拟器,抛出异常:

 java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.

程序崩溃原因:主线程(UI)线程中不能访问数据库,访问数据库是个耗时的任务,堵塞主线程。

应用线程

  • 主线程:处于一个循环的运行状态,主要响应UI相关事件,故也称UI线程
  • 后台线程:主线程外的其它线程,

后台线程

添加后台线程的原则:

  • 所有耗时的任务都应该在后台线程上完成
  • UI只能在主线程上更新

Android上能让我们在后台线程上执行任务的方法:

  • 第24章 异步网络请求
  • 第25章 Handler 处理后台小任务
  • 第27章 WorkManager 执行周期性的后台任务
  • 第12章 Executor 来插入和更新数据库
  • 本章节 LiveData

使用 LiveData

LiveData 能在线程间传递数据,满足添加后台线程的原则。

Room 原生支持与 LiveData 协调工作

引用 LiveData 库

代码清单:app/build.grale

implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'

之前要使用 ViewModel 时已经添加依赖了,这不用再添加了。

修改创建数据访问对象(DAO)

代码清单:src/app/main/java/com.example.criminalintent/database/CrimeDao.kt

@Dao
interface CrimeDao {
    @Query("select * from crime")
    //fun getCrimes(): List<Crime>
    fun getCrimes(): LiveData<List<Crime>>

    @Query("select * from crime where id=(:id)")
    //fun getCrime(id: UUID): Crime?
   fun getCrime(id: UUID): LiveData<Crime?>
}

从DAO 类返回ListData 实例,就是告诉Room 要在后台线程上执行数据库查询。
查询到 crime 数据后,LiveData 对象会把结果发送到主线程并通知 UI观察者。

修改仓储类

修改仓储类的查询函数,返回值为 ListData<T> 类型:

  • List<List<Crime>>
  • List<Crime>

代码清单:src/app/main/java/com.example.criminalintent/CrimeRepository.kt

  class CrimeRepository private constructor(context: Context) {
    ...
    //fun getCrimes(): List<Crime> = crimeDao.getCrimes()
    fun getCrimes(): LiveData<List<Crime>> = crimeDao.getCrimes()

    //fun getCrime(id: UUID): Crime? = crimeDao.getCrime(id)
    fun getCrime(id: UUID): LiveData<Crime?> = crimeDao.getCrime(id)
    ...
 }

观察 LiveData

重构名字

把 ViewModel中 crimes 的重命名为更贴切的名字: crimeLiveData,
代码清单:src/app/main/java/com.example.criminalintent/CrimeListViewModel.kt

class CrimeListViewModel : ViewModel() {
    private val crimeRepository: CrimeRepository = CrimeRepository.get()
    //var crimes = crimeRepository.getCrimes()
    var crimesLiveData = crimeRepository.getCrimes()
}

修改更新 updateUI 函数

代码清单:src/app/main/java/com.example.criminalintent/CrimeListFragment.kt

    private fun updateUI(crimes: List<Crime>) {
        adapter = CrimeAdapter(crimes)
        crimeRecycleView.adapter = adapter
    }

添加 LiveData 观察者

代码清单:src/app/main/java/com.example.criminalintent/CrimeListFragment.kt

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        crimeListViewModel.crimesLiveData.observe(
            viewLifecycleOwner,  // 保证Observer与fragment生命周期同步
            Observer {  crimes -> // 在LiveData的数据变更时执行
                crimes?.let {
                    Log.i(TAG, "Got crimes ${crimes.size}")
                    updateUI(crimes)
                }
            }
        )
    }
  • 是在 onViewCreated()写代码,是保证 fragment的视图已经创建完成后,才能更新视图UI。
  • 调用函数 crimeListViewModel.crimesLiveData.observe(生命周期拥有者,Observer { ... }) 添加 LiveData 的观察者
  • viewLifecycleOwner:生命周期拥有者,是Fragment内置成员(注意:是fragment的视图生命周期拥有者,而不是 fragment本身,不过默认这两者也是生命周期一致的),
    传入该值参保证Observer与fragment生命周期同步,在fragment被销毁后,解除Observer与LiveData的订阅关系,与其拥有者视图共存亡。
  • Observer { ... } : 在LiveData的数据变更时执行

运行结果

数据库的Schema

运行程序后,有条警告

Schema export directory is not provided to the annotation processor so we cannot export the schema. 
You can either provide `room.schemaLocation` annotation processor argument OR set exportSchema to false.

数据库的Schema 就是数据库结果,包含的注意元素:

  • 数据库里有哪些表
  • 表里有哪些栏位
  • 表与表间的关系和约束

Room 支持导出数据库的Schema 到一个文件, 保存在版本控制系统中进行版本的历史控制。

要消除上面的警告,有两个方法:

  1. 提供文件路径,保存Schema
    在 app/build.gradle 文件添加 kapt {}代码块
...
android {
    compileSdk 32
    buildTypes {
      ...
    }
    ...
    kapt {
        arguments {
            arg("room.schemaLocation", "$projectDir/schemas".toString())
        }
    }
   

添加后记得,Sync Now
再次运行模拟器,警告没有了

而这时,项目中多出了一个目录 app/schemas/com.example.criminalintent.database.CrimeDatabase

文件1.json的内容如下:

{
  "formatVersion": 1,
  "database": {
    "version": 1,
    "identityHash": "2de443d76b568d6e694b91d2e7d7d3e3",
    "entities": [
      {
        "tableName": "Crime",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `date` INTEGER NOT NULL, `isSolved` INTEGER NOT NULL, PRIMARY KEY(`id`))",
        "fields": [
          {
            "fieldPath": "id",
            "columnName": "id",
            "affinity": "TEXT",
            "notNull": true
          },
          {
            "fieldPath": "title",
            "columnName": "title",
            "affinity": "TEXT",
            "notNull": true
          },
          {
            "fieldPath": "date",
            "columnName": "date",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "isSolved",
            "columnName": "isSolved",
            "affinity": "INTEGER",
            "notNull": true
          }
        ],
        "primaryKey": {
          "columnNames": [
            "id"
          ],
          "autoGenerate": false
        },
        "indices": [],
        "foreignKeys": []
      }
    ],
    "views": [],
    "setupQueries": [
      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2de443d76b568d6e694b91d2e7d7d3e3')"
    ]
  }
}
  1. 禁用 schema导出功能,可以将 exportSchema 设置为 false:
    代码清单:src/app/main/java/com.example.criminalintent/database/CrimeDatabase.kt
@Database(entities = [ Crime::class], version = 1, exportSchema = false)
@TypeConverters(CrimeTypeConverters::class)
abstract class CrimeDatabase : RoomDatabase() {
    abstract fun crimeDao(): CrimeDao
}

推荐阅读