《Android编程权威指南》之数据库与Room库

本章的内容要学习数据库了,这里将学习 Room 库,当然还有别的对数据库进行操作的好用的库,可自行学习,大同小异。

一般来讲,非UI相关数据要么保存在本地(本地文件系统,或者是稍后就要为CriminalIntent创建的数据库),要么保存在Web服务器上。

官方 Room 介绍地址:https://developer.android.com/training/data-storage/room

Room 架构组建库

Room 是一个Jetpack架构组件库,在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。

Room API 包含一些用来定义数据库和创建数据库实例的类。注解类用来确定哪些类需要保存在数据库里,哪个类代表数据库,哪个类指定数据库表访问函数这样的事情。编译器负责处理注解类,生成数据库实现代码。

首先在 build.gradle 文件中添加相关依赖:

  plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
  }


    def room_version = "2.3.0"

    implementation "androidx.room:room-runtime:$room_version"
    implementation "androidx.room:room-ktx:$room_version"
    kapt "androidx.room:room-compiler:$room_version"

创建数据库

  • 1、注解模型类,使之成为一个数据库实体

    Room 基于实体类为应用构建数据库表,使用@Entity注解实体列,然后交给Room处理,一张数据库表就诞生了。

    /**
    * Crime 陋习实体类
    *
    * @property id
    * @property title
    * @property date 日期
    * @property isSolved 是否解决
    * @property requiresPolice 是否需警方介入
    * @constructor Create empty Crime
    */
    @Entity
    data class Crime(
       @PrimaryKey  val id: UUID = UUID.randomUUID(),
       var title: String = "",
       var date: Date = Date(),
       var isSolved: Boolean = false,
       var requiresPolice: Boolean = false
    )
    

    @Entity是个类级别的注解,这个注解表示被注解的类定义了一张或多张数据库表结构,这里,数据库表的每一条记录就代表一个Crime对象。属性名对应表字段名。

    @PrimaryKey注解指定数据库里哪一个字段是主键(primary key),具有唯一性,可以用来查找单条记录。

  • 2、创建数据库类

    @Database(entities = [Crime::class], version = 1)
    abstract class CrimeDatabase : RoomDatabase() {
    }
    

    @Database注解告诉Room,CrimeDatabase类就是应用里的数据库。

    参数「1」表示实体类集合,告诉Room在创建和管理数据库表时该用哪个实体类。
    参数「2」表示数据库版本。

  • 3、创建类型转换器,让数据库能够处理模型数据

    Room的后台数据库引擎是SQLite(Structured Query Language,开源关系型数据库)

    SQLite使用手册参考:www.sqlite.org

    类型转换器会告诉Room如何转换要保存的特定类型的数据。

    先定义好数据类型转换类 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 toUUID(uuid: String?): UUID {
          return UUID.fromString(uuid)
      }
    
      @TypeConverter
      fun fromUUID(uuid: UUID?): String? {
          return uuid?.toString()
       }
    }
    

    在CrimeDatabase中添加@TypeConverters注解,并传入CrimeTypeConverters类,意思就是告诉数据库,需要转换数据类型时,请使用CrimeTypeConverters类里的函数。

    @Database(entities = [Crime::class], version = 1)
    @TypeConverters(CrimeTypeConverters::class)
    abstract class CrimeDatabase : RoomDatabase() {
    }
    

好啦,数据库和数据库表的定义完成啦!

定义数据库访问对象

添加 CrimeDao.kt 接口类,定义对数据库进行操作的函数。@Dao注解告诉Room,CrimeDao是一个数据访问对象,Room会自动给CrimeDao接口里的函数生成实现代码。

@Dao
interface CrimeDao {

   @Query("SELECT * FROM crime")
   fun getCrimes(): List

   @Query("SELECT * FROM crime WHERE id = (:id)")
   fun getCrime(id: UUID): Crime?
}
@Database(entities = [Crime::class], version = 1)
@TypeConverters(CrimeTypeConverters::class)
abstract class CrimeDatabase : RoomDatabase() {
    abstract fun crimeDao():CrimeDao
}

使用仓库模式访问数据库

仓库类意思就是封装了一层从单个或多个数据源访问数据的一套逻辑。它决定如何读取和保存数据,无论是从本地数据库,还是远程服务器。UI代码直接从仓库获得要使用的数据,不关心如何与数据库打交道。

private const val DATABASE_NAME = "crime-database"

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 = 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 initialized")
       }
   }
}

CrimeRepository 使用了单例(singleton)模式创建,在整个应用进程中,将只有一个实例对象。

class CriminalIntentApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        CrimeRepository.initialize(this)
    }

}

在 CriminalIntentApplication 里面进行 CrimeRepository 初始化任务。记得在 AndroidManifest.xml 的 标签里把 CriminalIntentApplication 注册进去,替换系统默认的 Application。

测试数据库访问

这里讲数据库数据存入到了应用中,然后直接进行获取访问,发生了崩溃,由于要引出下一小节内容,像对数据库这种耗时任务,应该放入子线程中。

demo 中会直接先插入一些数据。没有采用载入数据库文件的方式提供数据源。

应用线程

Room 不允许在主线程上执行任何数据库操作。如强行为之,Room就会抛出java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time. 异常。

线程是一个单一执行序列。单个线程中的代码会逐步执行。所有Android应用的运行都是从主线程开始的。主线程并不是像线程那样的预定执行序列。相反,它处于一个无限循环的运行状态,等着用户或系统触发事件。

一般线程与主线程

由于响应的事件基本都与UI相关,因此主线程有时也叫UI线程。

  • 后台线程

    像数据库访问比较耗时,如果在主线程操作,等待太久,就会导致应用无响应(application not responding,ANR)

    应用无响应

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

使用LiveData

LiveData 是一种可观察的数据存储器类。

LiveData 官方概况地址:https://developer.android.com/topic/libraries/architecture/livedata

Room原生支持与LiveData协同工作。

Google开发LiveData的目的是让应用不同模块之间的数据传递简单一些,它还能支持在线程间传递数据。

在Room DAO里配置查询返回LiveData,Room会自动在后台线程上执行查询操作,完成后会把结果数据发布到LiveData对象。再配置activity或fragment来观察目标LiveData对象。这样,被观察的LiveData一准备就绪,activity或fragment就会在主线程上收到结果通知。

    fun getCrimes():LiveData> = crimeDao.getCrimes()

    fun getCrime(id:UUID):LiveData = crimeDao.getCrime(id)
class CrimeListViewModel : ViewModel() {

    private val crimeRepository = CrimeRepository.get()
    val crimesLiseLiveData = crimeRepository.getCrimes()

   init {

       GlobalScope.launch {
           for (i in 0 until 100) {
               val crime :Crime = Crime()
               crime.title = "Crime #$i"
               crime.isSolved = i % 2 != 0
               crime.requiresPolice = i % 2 == 0
               crimeRepository.insertCrimes(crime)
           }
       }
    }
}

LiveData.observe(LifecycleOwner, Observer)函数用来给LiveData实例注册观察者,让观察者和类似activity或fragment这样的其他组件同呼吸共命运。

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        crimeListViewModel.crimesLiseLiveData.observe(
            viewLifecycleOwner,
            Observer { crimes ->
                crimes?.let{
                    updateUI(crimes)
                }
            }
        )
    }

AndroidX版Fragment就是一个生命周期拥有者。它实现了LifecycleOwner接口,有一个表示fragment实例生命周期状态的Lifecycle对象。

具体代码参考结尾处 Github 地址。

挑战练习:解决Schema警告

仔细翻查项目的构建日志,会看到一条警告说应用没有提供schema导出目录.

数据库 schema 就是数据库结构,其包含的主要元素有:数据库里有哪些数据表、这些表里有哪些栏位,以及数据表之间的关系和约束是什么。

Room支持导出数据库schema到一个文件。这很有用,因为你可以把它保存在版本控制系统中进行版本历史控制。

要消除构建日志中的Schema警告,有两种方式:

  • 1、给@Database注解提供schema文件保存位置

    在app/build.gradle文件里添加以下kapt{}代码块:

    android {
        ...
        kapt{
            arguments{
                arg("room.schemaLocation", "...地址位置...")
            }
        }
    }
    
  • 2、禁用schema导出功能

    将exportSchema设置为false

    @Database(entities = [Crime::class], version = 1, exportSchema = false)
    @TypeConverters(CrimeTypeConverters::class)
    abstract class CrimeDatabase : RoomDatabase() {
        abstract fun crimeDao():CrimeDao
    }
    

深入学习:单例

单例指的是在整个应用中只有一个实例对象。该类负责创建自己的对象,同时确保只有单个对象被创建。

它可以控制实例数目,节省系统资源,很方便使用。

由于它的生命周期会比较长,所以它不适合做持久存储。

这里在强调,不要滥用单例模式。

Kotlin中单例的5种写法参考博文:https://juejin.cn/post/6844903590545326088

其他

CriminalIntent 项目 Demo 地址:
https://github.com/visiongem/AndroidGuideApp/tree/master/CriminalIntent

你可能感兴趣的:(《Android编程权威指南》之数据库与Room库)