《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<Crime>

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

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

class CriminalIntentApplication : Application() {

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

}

在 CriminalIntentApplication 里面进行 CrimeRepository 初始化任务。记得在 AndroidManifest.xml 的 <application> 标签里把 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<List<Crime>> = crimeDao.getCrimes()

    fun getCrime(id:UUID):LiveData<Crime?> = 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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容