此 Codelab 是“Android Kotlin 基础知识”课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘此课程的价值。“Android Kotlin 基础知识”Codelab 着陆页列出了所有课程 Codelab。
简介
大多数应用都有需要保留的数据,即使在用户关闭应用后仍要继续保留数据。例如,应用可能会存储播放列表、游戏物品清单、支出和收入记录、星座目录或一段时间内的睡眠数据。通常,您会使用数据库来存储持久性数据。
Room
是一个数据库,属于 Android Jetpack 的一部分。Room
负责处理数据库设置和配置方面的许多繁琐工作,并让您的应用可以使用普通函数调用与数据库进行交互。在后台,Room
是 SQLite 数据库之上的一个抽象层。Room
的术语和复杂查询的查询语法采用 SQLite 模型。
下图展示了 Room
数据库如何融入本课程中推荐的总体架构。
您应当已掌握的内容
您应熟悉以下内容:
- 为 Android 应用构建基本界面
- 使用 activity、fragment 和视图。
- 在 fragment 之间导航,并使用 Safe Args(一个 Gradle 插件)在 fragment 之间传递数据。
- 视图模型、视图模型工厂以及
LiveData
及其观察者。本课程前面的 Codelab 介绍了这些架构组件主题。 - SQL 数据库和 SQLite 语言方面的基础知识。如需快速概览或回顾相关知识,请参阅 SQLite Primer。
学习内容
- 如何创建
Room
数据库并与之交互以保留数据。 - 如何创建用于在数据库中定义表的数据类。
- 如何使用数据访问对象 (DAO) 将 Kotlin 函数映射到 SQL 查询?
- 如何测试数据库是否正常运行。
您将执行的操作
- 创建一个包含每晚睡眠数据接口的
Room
数据库。 - 使用提供的测试测试数据库。
在此 Codelab 中,您将为一款用于跟踪睡眠质量的应用构建数据库部分。该应用使用数据库来存储长期睡眠数据。
该应用包含两个由 Fragment 表示的屏幕,如下图所示。
左侧所示的第一个屏幕包含用于开始和停止跟踪的按钮。这个屏幕会显示用户的所有睡眠数据。点击清除按钮会永久删除应用为用户收集的所有数据。
右侧所示的第二个屏幕用于选择睡眠质量评分。在该应用中,评分用数字表示。出于开发目的,该应用会显示人脸图标及其对应的数字。
用户的流程如下所示:
- 用户打开应用,系统会显示睡眠跟踪屏幕。
- 用户点按 START 按钮。系统会记录开始时间并显示该时间。START 按钮会停用,而 STOP 按钮会启用。
- 用户点按 STOP 按钮。这会记录结束时间并打开睡眠质量屏幕。
- 用户选择一个睡眠质量图标。这个屏幕会关闭,跟踪屏幕会显示睡眠结束时间和睡眠质量。STOP 按钮会停用,而 START 按钮会启用。此应用还有一晚就可以使用了。
- 只要数据库中有数据,CLEAR 按钮就会处于启用状态。如果用户点按 CLEAR 按钮,系统会清空其所有数据,并且不予追偿,也就是说,系统不会显示“您确定吗?”这类消息。
该应用在完整架构的基础上采用简化的架构,如下所示。该应用仅使用以下组件:
- 界面控制器
- 视图模型和
LiveData
- Room 数据库
第 1 步:下载并运行起始应用
- 从 GitHub 下载 TrackMySleepQuality-Starter 应用。
- 构建并运行应用。该应用会显示
SleepTrackerFragment
fragment 的界面,但不会显示数据。按钮不会响应点按操作。
第 2 步:检查起始应用
- 查看 Gradle 文件:
- 项目 Gradle 文件
在项目级build.gradle
文件中,请注意指定库版本的变量。起始应用中所用的版本能够很好地协同工作,并能与此应用完美配合。当您完成此 Codelab 时,Android Studio 可能会提示您更新部分版本。您需要更新还是保留应用中的版本由您决定。如果遇到编译异常,请尝试组合使用最终解决方案应用使用的库版本。 - 模块 Gradle 文件。请注意,它提供了所有 Android Jetpack 库(包括
Room
)的依赖项,以及协程的依赖项。
- 请查看软件包和界面。。应用的结构按功能而定。该软件包包含占位符文件,您将在这一系列 Codelab 中添加代码。
database
软件包,适用于与Room
数据库相关的所有代码。sleepquality
和sleeptracker
软件包包含每个屏幕的 fragment、视图模型和视图模型工厂。
- 查看
Util.kt
文件,其中包含有助于显示睡眠质量数据的函数。部分代码已被注释掉,因为它引用了您稍后创建的视图模型。 - 请查看 androidTest 文件夹 (
SleepDatabaseTest.kt
)。您将使用此测试来验证数据库是否按预期运行。
在 Android 中,数据以数据类表示,并且系统通过函数调用来访问和修改数据。但是,在数据库环境中,您需要实体和查询。
- 实体表示要存储在数据库中的对象或概念及其属性。entity 类定义一个表,该类的每个实例表示该表中的一行。每个属性定义一个列。在您的应用中,实体将保存有关夜间睡眠的信息。
- 查询是从一个数据库表或多个表的组合中获取数据或信息的请求,或对数据执行操作的请求。常见查询用于获取、插入和更新实体。例如,您可以查询已记录的所有睡眠晚数(按开始时间排序)。
Room
会为您完成所有从 Kotlin 数据类到可存储在 SQLite 表中的实体的工作,以及从函数声明到 SQL 查询的所有工作。
您必须将每个实体定义为带有注解的数据类,并将互动定义为带有注解的接口,即数据访问对象 (DAO)。Room
使用这些带注解的类在数据库中创建表,以及对数据库执行操作的查询。
第 1 步:创建 SleepNight 实体
在此任务中,您将一个晚上的睡眠定义为带注解的数据类。
对于一晚的睡眠,您需要记录开始时间、结束时间和质量评分。
此外,您还需要一个可用于唯一标识相应夜晚的 ID。
- 在
database
软件包中,找到并打开SleepNight.kt
文件。 - 创建
SleepNight
数据类,其中包含 ID、开始时间(以毫秒为单位)、结束时间(以毫秒为单位)和数字睡眠质量评分参数。
- 您必须初始化
sleepQuality
,因此将其设置为-1
,以表示系统未收集到质量数据。 - 您还必须初始化结束时间。将其设置为开始时间,以表明尚未记录任何结束时间。
data class SleepNight(
var nightId: Long = 0L,
val startTimeMilli: Long = System.currentTimeMillis(),
var endTimeMilli: Long = startTimeMilli,
var sleepQuality: Int = -1
)
- 在类声明的上方,为该数据类添加
@Entity
注解。将表命名为daily_sleep_quality_table
。tableName
的参数为可选参数,但我们建议您使用。您可以在文档中查找其他参数。
如果系统提示,请导入androidx
库中的Entity
和所有其他注解。
@Entity(tableName = "daily_sleep_quality_table")
data class SleepNight(...)
- 如需将
nightId
标识为主键,请为nightId
属性添加@PrimaryKey
注解。将参数autoGenerate
设为true
,让Room
为每个实体生成 ID。这保证了每晚的 ID 唯一性。
@PrimaryKey(autoGenerate = true)
var nightId: Long = 0L,...
- 为其余属性添加
@ColumnInfo
注解。如下所示,使用参数自定义属性名称。
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "daily_sleep_quality_table")
data class SleepNight(
@PrimaryKey(autoGenerate = true)
var nightId: Long = 0L,
@ColumnInfo(name = "start_time_milli")
val startTimeMilli: Long = System.currentTimeMillis(),
@ColumnInfo(name = "end_time_milli")
var endTimeMilli: Long = startTimeMilli,
@ColumnInfo(name = "quality_rating")
var sleepQuality: Int = -1
)
- 构建并运行您的代码,以确保它没有错误。
在此任务中,您将定义一个数据访问对象 (DAO)。在 Android 中,DAO 提供了插入、删除和更新数据库的便捷方法。
使用 Room
数据库时,您需要通过在代码中定义和调用 Kotlin 函数来查询数据库。这些 Kotlin 函数会映射到 SQL 查询。您可以使用注解在 DAO 中定义这些映射,然后 Room
会创建必要的代码。
可以将 DAO 视为定义用于访问数据库的自定义接口。
对于常见的数据库操作,Room
库会提供方便的注解,例如 @Insert
、@Delete
和 @Update
。对于所有其他操作,都使用 @Query
注解。您可以编写 SQLite 支持的任何查询。
另一个好处是,当您在 Android Studio 中创建查询时,编译器会检查您的 SQL 查询是否存在语法错误。
对于睡眠之夜的睡眠跟踪器数据库,您必须能够执行以下操作:
- 插入新夜晚。
- 更新现有夜晚的结束时间和质量评分。
- 根据键获取特定夜晚的数据。
- 获取所有夜晚时间,以便显示这些夜晚。
- 获取最近一晚的数据。
- 删除数据库中的所有条目。
第 1 步:创建 SleepDatabase DAO
- 在
database
软件包中,打开SleepDatabaseDao.kt
。 - 请注意,
interface
SleepDatabaseDao
带有@Dao
注解。所有 DAO 都需要使用@Dao
关键字进行注解。
@Dao
interface SleepDatabaseDao {}
- 在该接口的主体内添加
@Insert
注解。在@Insert
下方,添加一个insert()
函数,该函数将Entity
类SleepNight
的实例作为其参数。
大功告成!Room
会生成将SleepNight
插入数据库所需的所有代码。从 Kotlin 代码调用insert()
时,Room
会执行 SQL 查询以将实体插入到数据库中。(注意:您可以随意调用该函数。)
@Insert
fun insert(night: SleepNight)
- 添加
@Update
注解以及将一个SleepNight
作为参数的update()
函数。更新的实体是与所传入实体具有相同键的实体。您可以更新该实体的部分或全部其他属性。
@Update
fun update(night: SleepNight)
其余功能没有方便使用的注解,因此您必须使用 @Query
注解并提供 SQLite 查询。
- 添加
@Query
注解以及一个get()
函数,该函数接受Long
key
参数并返回可为 null 的SleepNight
。您会看到表示参数缺失的错误。
@Query
fun get(key: Long): SleepNight?
- 查询以字符串参数的形式提供给注解。将参数添加到
@Query
。将其设为String
(这是一个 SQLite 查询)。
- 选择
daily_sleep_quality_table
中的所有列 WHERE
语句中的nightId
匹配 :key
参数。
注意:key
。在查询中使用英文冒号是为了引用该函数中的参数。
("SELECT * from daily_sleep_quality_table WHERE nightId = :key")
- 再添加一个
@Query
,以及用于从daily_sleep_quality_table
中删除 (DELETE
) 所有信息的clear()
函数和 SQLite 查询。此查询不会删除表本身。@Delete
注解会删除一个商品。您可以使用@Delete
提供要删除的住宿列表。这种方法的缺点是,您需要提取或了解表中的内容。@Delete
注解非常适合删除特定条目,但并不利于清除表中的所有条目。
@Query("DELETE FROM daily_sleep_quality_table")
fun clear()
- 添加
@Query
注解和一个getTonight()
函数。使getTonight()
返回的SleepNight
可为 null,以便函数能够处理表为空的情况。(该表格的开头位于空白处,在清除数据后为空)。
要从数据库中获取“今晚”,写入 SQLite 查询,以nightId
返回按降序排序的结果列表的第一个元素。使用LIMIT 1
可仅返回一个元素。
@Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC LIMIT 1")
fun getTonight(): SleepNight?
- 添加一个具有
getAllNights()
函数的@Query
:
- 让 SQLite 查询返回
daily_sleep_quality_table
中的所有列,依降序排序。 - 让
getAllNights()
返回SleepNight
实体的列表作为LiveData
。Room
会为您保持更新此LiveData
,也就是说,您只需要显式获取一次数据。 - 您可能需要从
androidx.lifecycle.LiveData
导入LiveData
。
@Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC")
fun getAllNights(): LiveData<List<SleepNight>>
- 尽管您不会看到任何明显的更改,但还是应运行应用以确保其没有错误。
在此任务中,您将创建一个 Room
数据库,该数据库使用您在上一个任务中创建的 Entity
和 DAO。
您需要创建一个抽象数据库容器类,并为其添加 @Database
注解。这个类有一种方法,能够在数据库不存在时创建数据库的一个实例,或者返回对现有数据库的引用。
获取 Room
数据库的操作有点麻烦,因此在您开始编码之前,请按照以下常规流程操作:
- 创建一个用于执行
extends RoomDatabase
操作的public abstract
类。此类将充当数据库容器。它是抽象类,因为Room
会为您创建实现。 - 为该类添加
@Database
注解。在参数中,为数据库声明实体并设置版本号。 - 在
companion
对象内,定义一个返回SleepDatabaseDao
的抽象方法或属性。Room
将为您生成主体。 - 整个应用只需要一个
Room
数据库实例,因此请将RoomDatabase
设为单例。 - 使用
Room
的数据库构建器,以仅在数据库不存在时创建数据库。否则,请返回现有数据库。
第 1 步:创建数据库
- 在
database
软件包中,打开SleepDatabase.kt
。 - 在该文件中,创建一个名为
SleepDatabase
的abstract
类,用于扩展RoomDatabase
。
使用@Database
为该类添加注解。
@Database()
abstract class SleepDatabase : RoomDatabase() {}
- 您会看到表示实体和版本参数缺失的错误。
@Database
注解需要包含几个参数,Room
才能构建数据库。
- 将
SleepNight
作为唯一项提供entities
列表。 - 将
version
设为1
。每当您更改架构时,都必须增加版本号。 - 将
exportSchema
设为false
,这样就不会保留架构版本记录的备份。
entities = [SleepNight::class], version = 1, exportSchema = false
- 该数据库需要知悉 DAO。在类的主体内,声明一个返回
SleepDatabaseDao
的抽象值。您可以有多个 DAO。
abstract val sleepDatabaseDao: SleepDatabaseDao
- 在它下面,定义
companion
对象。伴生对象允许客户端访问用于创建或获取数据库,而无需该类进行实例化的方法。由于该类的唯一用途是提供数据库,因此没有必要对它进行实例化。
companion object {}
- 在
companion
对象内,为数据库声明一个私有的可为 null 变量INSTANCE
,并将其初始化为null
。INSTANCE
变量将在数据库创建后保留对数据库的引用。这有助于避免重复打开与数据库的连接成本高昂的情况。
为 INSTANCE
添加 @Volatile
注解。volatile 变量的值绝不会缓存,所有读写操作都将在主内存中完成。这有助于确保 INSTANCE
的值始终是最新的值,并且对所有执行线程都相同。也就是说,一个线程对 INSTANCE
所做的更改会立即对所有其他线程可见,并且不会出现这样的情况:比如说,两个线程都更新缓存中的同一个实体,这会造成问题。
@Volatile
private var INSTANCE: SleepDatabase? = null
- 在
INSTANCE
下,仍在companion
对象内,定义getInstance()
方法并提供数据库构建器所需的Context
参数。返回类型SleepDatabase
。您将看到一条错误,因为getInstance()
尚不会返回任何内容。
fun getInstance(context: Context): SleepDatabase {}
- 在
getInstance()
内,添加synchronized{}
代码块。传入this
以便您可以访问上下文。
多个线程可能会同时请求一个数据库实例,从而导致生成两个数据库而不是一个。此问题在示例应用中不太可能发生,但对于更复杂的应用,这个问题可能发生。将代码封装到synchronized
中意味着一次只有一个执行线程可以进入此代码块,从而确保数据库仅初始化一次。
synchronized(this) {}
- 在 synchronized 代码块内,将
INSTANCE
的当前值复制到局部变量instance
中。这是利用智能类型转换(仅适用于本地变量)。
var instance = INSTANCE
- 在
synchronized
代码块内,在synchronized
代码块的末尾执行return instance
操作。忽略返回值类型不匹配错误;操作完成后,永远不会返回 null。
return instance
- 在
return
语句上方添加if
语句,用于检查instance
是否为 null;如果为 null,即表示还没有数据库。
if (instance == null) {}
- 如果
instance
为null
,请使用数据库构建器获取数据库。在if
语句的正文中,调用Room.databaseBuilder
并提供您传入的上下文、数据库类以及数据库的名称sleep_history_database
。要消除此错误,您必须在以下步骤中添加迁移策略和build()
。
instance = Room.databaseBuilder(
context.applicationContext,
SleepDatabase::class.java,
"sleep_history_database")
- 将所需的迁移策略添加到构建器中。使用
.fallbackToDestructiveMigration()
。
通常,您必须为迁移对象提供在架构发生更改时使用的迁移策略。迁移对象是一种对象,用于指定如何采用旧架构将所有行转换为新架构中的行,以确保数据不会丢失。迁移不在此 Codelab 的范围内。一种简单的解决办法是销毁并重新构建数据库,这意味着数据会丢失。
.fallbackToDestructiveMigration()
- 最后,调用
.build()
。
.build()
- 将
INSTANCE = instance
语句指定为if
语句中的最后一步。
INSTANCE = instance
- 您的最终代码应如下所示:
@Database(entities = [SleepNight::class], version = 1, exportSchema = false)
abstract class SleepDatabase : RoomDatabase() {
abstract val sleepDatabaseDao: SleepDatabaseDao
companion object {
@Volatile
private var INSTANCE: SleepDatabase? = null
fun getInstance(context: Context): SleepDatabase {
synchronized(this) {
var instance = INSTANCE
if (instance == null) {
instance = Room.databaseBuilder(
context.applicationContext,
SleepDatabase::class.java,
"sleep_history_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
}
return instance
}
}
}
}
- 构建并运行代码。
现在,您已拥有使用 Room
数据库所需的全部构建块。该代码会编译并运行,但您无法判断它是否确实能正常运行。因此,现在是添加一些基本测试的好时机。
第 2 步:测试 SleepDatabase
在此步骤中,您将运行提供的测试,以验证您的数据库是否正常运行。这样一来,您可以在确保数据库正常运行之后再基于其进行构建。提供的测试为基本测试。对于生产应用,您需要练习所有 DAO 中的所有函数和查询。
起始应用包含 androidTest 文件夹。此 androidTest 文件夹包含的单元测试涉及 Android 插桩;这个华丽的术语表示测试需要用到 Android 框架,因此您需要在实体或虚拟设备上运行测试。当然,您还可以创建并运行不涉及 Android 框架的纯单元测试。
- 在 Android Studio 的 androidTest 文件夹中,打开 SleepDatabaseTest 文件。
- 如需取消注释代码,请选择所有被注释的代码,然后按
Cmd+/
或Control+/
键盘快捷键。 - 查看这个文件。
这是测试代码的快速演示,因为它是您可以重复使用的另一段代码:
SleepDabaseTest
是一个测试类。@RunWith
注解用于标识测试运行程序,即设置和执行测试的程序。- 在设置期间,系统会执行带有
@Before
注解的函数,这个函数会使用SleepDatabaseDao
创建一个内存中SleepDatabase
。“内存中”表示此数据库不会保存到文件系统中,在测试运行完毕后会被删除。 - 同样,在构建内存中数据库时,代码会调用另一个测试专用方法
allowMainThreadQueries
。默认情况下,如果您尝试在主线程上运行查询,会遇到错误。使用此方法,您可以在主线程上运行测试,而这只能在测试期间执行。 - 在带有
@Test
注解的测试方法中,您可以创建、插入和检索SleepNight
,还可以断言它们是相同的。出现任何问题时会抛出异常。在实际测试中,您会有多个@Test
方法。 - 测试完成后,系统会执行带有
@After
注解的函数,以关闭数据库。
- 右键点击 Project 窗格中的测试文件,然后选择 Run 'SleepDatabaseTest'。
- 运行测试后,在 SleepDatabaseTest 窗格中验证所有测试均已通过。
因为所有测试均已通过,所以现在您了解了以下几方面的情况:
- 数据库已正确创建。
- 您可以在数据库中插入
SleepNight
。 - 您可以恢复
SleepNight
。 SleepNight
具有正确的质量值。
Android Studio 项目:TrackMySleepQualityRoomAndTesting
在测试数据库时,您需要练习 DAO 中定义的所有方法。如需完成测试,请添加并执行测试以执行其他 DAO 方法。
- 将您的表定义为带有
@Entity
注解的数据类。将带有@ColumnInfo
注解的属性定义为表中的列。 - 将数据访问对象 (DAO) 定义为带有
@Dao
注解的接口。DAO 用于将 Kotlin 函数映射到数据库查询。 - 使用注解来定义
@Insert
、@Delete
和@Update
函数。 - 将
@Query
注解与 SQLite 查询字符串作为任何其他查询的参数。 - 创建一个抽象类,它具有可返回数据库的
getInstance()
函数。 - 使用插桩测试来测试数据库和 DAO 是否按预期运行。您可以将提供的测试作为模板。
Udacity 课程:
Android 开发者文档:
其他文档和文章:
- 单例模式
- 来自 Google 开发者专家:在正确使用 volatile 和同步时
- 伴生对象
- 了解借助 Room 进行的迁移
- 测试 Room 迁移
- 数据库的历史
- SQLite 网站
- SQLite 所理解的 SQL 的完整说明
此部分列出了在由讲师主导的课程中,学生学习此 Codelab 后可能需要完成的家庭作业。讲师自行决定是否执行以下操作:
- 根据需要布置作业。
- 告知学生如何提交家庭作业。
- 给家庭作业评分。
讲师可以酌情采纳这些建议,并且可以自由布置自己认为合适的任何其他家庭作业。
如果您是在自学此 Codelab,可随时通过这些家庭作业来检测您的知识掌握情况。
回答以下问题
问题 1
如何指明某个类代表要存储在 Room
数据库中的实体?
- 使该类扩展
DatabaseEntity
。 - 为该类添加
@Entity
注解。 - 为该类添加
@Database
注解。 - 使该类扩展
RoomEntity
,并为该类添加@Room
注解。
问题 2
DAO(数据访问对象)是一个接口,Room
会使用该接口将 Kotlin 函数映射到数据库查询。
如何指明某个接口代表 Room
数据库的 DAO?
- 使该接口扩展
RoomDAO
。 - 使接口扩展
EntityDao
,然后实现DaoConnection()
方法。 - 为该接口添加
@Dao
注解。 - 为该接口添加
@RoomConnection
注解。
问题 3
以下关于 Room
数据库的说法中,哪些是正确的?请选择所有适用的选项。
- 您可以将
Room
数据库的表定义为带注解的数据类。 - 如果您从查询返回
LiveData
,则Room
会在LiveData
发生变化时为您更新LiveData
。 - 每个
Room
数据库必须有且只能有一个 DAO。 - 如需将某个类标识为
Room
数据库,请将其设为RoomDatabase
的子类,并为其添加@Database
注解。
问题 4
您可以在 @Dao
接口中使用以下哪些注解?请选择所有适用的选项。
@Get
@Update
@Insert
@Query
问题 5
如何验证数据库是否在正常运行?请选择所有适用的选项。
- 编写插桩测试。
- 继续编写和运行应用,直到显示数据。
- 将对 DAO 接口中的方法的调用替换为对
Entity
类中的等效方法的调用。 - 运行
Room
库提供的verifyDatabase()
函数。
开始学习下一课:
如需本课程中其他 Codelab 的链接,请参阅“Android Kotlin 基础知识”Codelab 着陆页。