使用 Room 持久保留数据

1. 准备工作

大多数正式版优质应用应用都有需要保存的数据,即使在用户关闭应用后仍要继续留存数据。例如,应用可能会存储歌曲播放列表、待办事项列表中的待办事项、支出和收入记录、星座目录或个人数据的历史记录。在大多数此类情况下,您都会使用数据库来存储这种持久性数据。

Room 是一个持久性库,属于 Android Jetpack 的一部分。Room 是 SQLite 数据库之上的一个抽象层。SQLite 使用一种专门的语言 (SQL) 来执行数据库操作。Room 并不直接使用 SQLite,而是负责简化数据库设置和配置以及与数据库交互方面的琐碎工作。此外,Room 还提供 SQLite 语句的编译时检查。

下图展示了 Room 如何融入本课程中推荐的总体架构。

7521165e051cc0d4.png

前提条件

  • 了解如何为 Android 应用构建基本界面 (UI)。
  • 了解如何使用 activity、fragment 和视图。
  • 了解如何在 fragment 之间导航,使用 Safe Args 在 fragment 之间传递数据。
  • 熟悉 Android 架构组件 ViewModelLiveDataFlow,并了解如何使用 ViewModelProvider.Factory 实例化 ViewModel。
  • 熟悉并发基础知识。
  • 了解如何使用协程管理长时间运行的任务。
  • 对 SQL 数据库和 SQLite 语言有基本的了解。

学习内容

  • 如何使用 Room 库创建 SQLite 数据库并与之交互?
  • 如何创建实体、DAO 和数据库类?
  • 如何使用数据访问对象 (DAO) 将 Kotlin 函数映射到 SQL 查询?

构建内容

  • 您将构建一个 Inventory 应用,将商品目录商品保存到 SQLite 数据库中。

所需条件

  • Inventory 应用的起始代码。
  • 一台安装了 Android Studio 的计算机。

2. 应用概览

在此 Codelab 中,您将使用名为 Inventory 的起始应用,并使用 Room 库为该应用添加数据库层。该应用的最终版本将使用 RecyclerView 显示商品目录数据库中商品的列表。用户可以选择在商品目录数据库中添加新商品、更新现有商品和删除商品(您将在下一个 Codelab 中完成该应用的功能)。

以下是该应用最终版本的屏幕截图。

439ad9a8183278c5.png

3. 起始应用概览

下载此 Codelab 的起始代码

此 Codelab 提供了起始代码,供您使用此 Codelab 中所教的功能对其进行扩展。起始代码可能既包含您在之前的 Codelab 中已熟悉的代码,也包含您不熟悉并将在后续 Codelab 中了解的代码。

如果您使用 GitHub 中的起始代码,请注意文件夹名称为 android-basics-kotlin-inventory-app-starter。在 Android Studio 中打开项目时,请选择此文件夹。

如需获取此 Codelab 的代码并在 Android Studio 中打开它,请执行以下操作。

获取代码

  1. 点击提供的网址。此时,项目的 GitHub 页面会在浏览器中打开。
  2. 在项目的 GitHub 页面上,点击 Code 按钮,这时会出现一个对话框。

5b0a76c50478a73f.png

  1. 在对话框中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
  2. 在计算机上找到该文件(可能在 Downloads 文件夹中)。
  3. 双击 ZIP 文件进行解压缩。系统将创建一个包含项目文件的新文件夹。

在 Android Studio 中打开项目

  1. 启动 Android Studio。
  2. Welcome to Android Studio 窗口中,点击 Open an existing Android Studio project

36cc44fcf0f89a1d.png

注意:如果 Android Studio 已经打开,请依次选择 File > New > Import Project 菜单选项。

21f3eec988dcfbe9.png

  1. Import Project 对话框中,找到解压缩的项目文件夹所在的位置(可能在 Downloads 文件夹中)。
  2. 双击该项目文件夹。
  3. 等待 Android Studio 打开项目。
  4. 点击 Run 按钮 11c34fc5e516fb1c.png 以构建并运行应用。请确保该应用按预期构建。
  5. Project 工具窗口中浏览项目文件,了解应用的设置方式。

起始代码概览

  1. 在 Android Studio 中打开包含起始代码的项目。
  2. 在 Android 设备或模拟器上运行应用。确保模拟器或连接的设备搭载的是 API 级别 26 或更高版本。Database Inspector 在搭载 API 级别 26 的模拟器/设备上使用效果最佳。
  3. 应用没有显示任何商品目录数据。请注意用于将新商品添加到数据库中的 FAB。
  4. 点击 FAB。应用会转到一个新屏幕,您可以在其中输入新商品的详细信息。

9c5e361a89453821.png

起始代码存在的问题

  1. Add Item 屏幕中,输入商品的详细信息。点按 Save。添加商品 fragment 未关闭。使用系统返回键返回。新商品不会保存,也不会列于 Inventory 屏幕上。请注意,该应用尚未完成,Save 按钮功能尚未实现。

f0931dab5089a14f.png

在此 Codelab 中,您将为一款用于将商品目录详细信息保存到 SQLite 数据库中的应用添加数据库部分。您将使用 Room 持久性库与 SQLite 数据库进行交互。

代码演示

您下载的起始代码已为您预先设计了屏幕布局。在本在线课程中,您只需专心实现数据库逻辑即可。下面简要介绍了一些文件,以帮助您上手。

main_activity.xml

主 activity,用于托管应用中的其他所有 fragment。onCreate() 方法从 NavHostFragment 检索 NavController,并设置操作栏以与 NavController 配合使用。

item_list_fragment.xml

应用中显示的第一个屏幕。此文件中主要包含一个 RecyclerView 和一个 FAB。在本在线课程的后面部分,您将实现该 RecyclerView。

fragment_add_item.xml

此布局包含一些文本字段,用于输入要添加的新商品目录商品的详细信息。

ItemListFragment.kt

此 fragment 主要包含样板代码。onViewCreated() 方法中对 FAB 设置了点击监听器,用于导航至添加商品 fragment。

AddItemFragment.kt

此 fragment 用于向数据库中添加新商品。onCreateView() 函数用于初始化绑定变量,onDestroyView() 函数用于在销毁 fragment 之前隐藏键盘。

4. Room 的主要组件

Kotlin 通过引入数据类提供了一种简单的数据处理方式。这种数据通过函数调用进行访问并可能会进行修改。不过,在数据库环境中,您需要通过表和查询来访问和修改数据。Room 的以下组件可以使这些工作流变得顺畅。

Room 包含三个主要组件:

  • 数据实体表示应用的数据库中的表。数据实体用于更新表中的行所存储的数据以及创建新行供插入。
  • 数据访问对象 (DAO) 提供应用在数据库中检索、更新、插入和删除数据所用的方法。
  • 数据库类持有数据库,并且是应用数据库底层连接的主要访问点。数据库类为应用提供与该数据库关联的 DAO 的实例。

稍后您将在此 Codelab 中实现并详细了解这些组件。下图演示了 Room 的各组件如何协同工作以与数据库交互。

33a193a68c9a8e0e.png

添加 Room 库

在此任务中,您将向 Gradle 文件中添加所需的 Room 组件库。

  1. 打开模块级 gradle 文件 build.gradle (Module: InventoryApp.app)。在 dependencies 块中,为 Room 库添加以下依赖项。
    // Room
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    implementation "androidx.room:room-ktx:$room_version"

5. 创建商品实体

实体类定义了一个表,该类的每个实例表示数据库表中的一行。实体类以映射告知 Room 它打算如何呈现数据库中的信息并与之交互。在您的应用中,实体将保存有关商品目录商品的信息,例如商品名称、商品价格和可用库存。

8c9f1659ee82ca43.png

@Entity 注解用于将某个类标记为数据库实体类。对于每个实体类,系统都会创建一个数据库表来保存相关项。除非另行说明,否则实体的每个字段在数据库中都表示为一列(如需了解详情,请参阅实体文档)。存储在数据库中的每个实体实例都必须有一个主键。主键用于唯一标识数据库表中的每个记录/条目。主键一旦赋值就不能修改,只要它还存在于数据库中,它就表示相应的实体对象。

在此任务中,您将创建一个实体类。请定义各个字段,用于存储每个商品的以下商品目录信息。

  • 一个用于存储主键的 Int
  • 一个用于存储商品名称的 String
  • 一个用于存储商品价格的 double
  • 一个用于存储库存数量的 Int
  1. 在 Android Studio 中打开起始代码。
  2. com.example.inventory 基础软件包下创建一个名为 data 的软件包。

be39b42484ba2664.png

  1. data 软件包内,创建一个名为 Item 的 Kotlin 类。此类将用于表示应用中的数据库实体。在下一步中,您将添加相应的字段来存储商品目录信息。
  2. 使用以下代码更新 Item 类定义。将类型为 Intid、类型为 String,itemName、类型为 DoubleitemPrice 和类型为 IntquantityInStock 声明为主构造函数的参数。为 id 赋默认值 0。这将是主键,即用于唯一标识 Item 表中每个记录/条目的 ID。
class Item(
   val id: Int = 0,
   val itemName: String,
   val itemPrice: Double,
   val quantityInStock: Int
)

数据类

数据类在 Kotlin 中主要用于保存数据。该类以关键字 data 进行标记。Kotlin 数据类对象有一些额外的优势,编译器会自动生成用于比较、输出和复制的实用程序,例如 toString()copy()equals()

示例

// Example data class with 2 properties.
data class User(val first_name: String, val last_name: String){
}

为了确保生成的代码的一致性,也为了确保其行为有意义,数据类必须满足以下要求:

  • 主构造函数至少需要有一个参数。
  • 主构造函数的所有参数都需要标记为 valvar
  • 数据类不能为 abstractopensealedinner

如需详细了解数据类,请参阅相关文档

  1. Item 类转换为数据类,方法是在其类定义前面加上 data 关键字作为前缀。
data class Item(
   val id: Int = 0,
   val itemName: String,
   val itemPrice: Double,
   val quantityInStock: Int
)
  1. Item 类声明的上方,为该数据类添加 @Entity 注解。使用 tableName 参数将 item 指定为 SQLite 表的名称。
@Entity(tableName = "item")
data class Item(
   ...
)
  1. 如需将 id 标识为主键,请为 id 属性添加 @PrimaryKey 注解。将参数 autoGenerate 设为 true,让 Room 为每个实体生成 ID。这样做可以保证每个商品的 ID 一定是唯一的。
@Entity(tableName = "item")
data class Item(
   @PrimaryKey(autoGenerate = true)
   val id: Int = 0,
   ...
)
  1. 为其余属性添加 @ColumnInfo 注解。ColumnInfo 注解用于自定义与特定字段关联的列。例如,使用 name 参数时,您可以为字段指定不同的列名称,而不是变量名称。如下所示,使用参数自定义属性名称。此方法类似于使用 tableName 为数据库指定不同的名称。
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity
data class Item(
   @PrimaryKey(autoGenerate = true)
   val id: Int = 0,
   @ColumnInfo(name = "name")
   val itemName: String,
   @ColumnInfo(name = "price")
   val itemPrice: Double,
   @ColumnInfo(name = "quantity")
   val quantityInStock: Int
)

6. 创建 item DAO

数据访问对象 (DAO)

数据访问对象 (DAO) 是一种模式,其作用是通过提供抽象接口将持久性层与应用的其余部分分离。这种分离遵循您曾在之前的 Codelab 中接触过的单一责任原则

DAO 的功能在于,让在底层持久性层执行数据库操作所涉及的所有复杂性都不波及应用的其余部分。这样就可以独立于使用数据的代码更改数据访问层。

7a8480711f04b3ef.png

在此任务中,您将为 Room 定义一个数据访问对象 (DAO)。数据访问对象是 Room 的主要组件,负责定义用于访问数据库的接口。

您要创建的 DAO 是一个自定义接口,提供用于查询/检索、插入、删除和更新数据库的便捷方法。Room 将在编译时生成该类的实现。

对于常见的数据库操作,Room 库会提供方便的注解,例如 @Insert@Delete@Update。对于所有其他操作,都使用 @Query 注解。您可以编写 SQLite 支持的任何查询。

另一个好处是,当您在 Android Studio 中编写查询时,编译器会检查您的 SQL 查询是否存在语法错误。

对于 Inventory 应用,您必须能够执行以下操作:

  • 插入或添加新商品。
  • 更新现有商品来更新名称、价格和数量。
  • 根据主键 id 获取特定商品。
  • 获取所有商品,让您可以显示它们。
  • 删除数据库中的条目。

bb381857d5fba511.png

现在,请在您的应用中实现 item DAO:

  1. data 软件包中,创建 Kotlin 类 ItemDao.kt
  2. 将类定义更改为 interface 并添加 @Dao 注解。
@Dao
interface ItemDao {
}
  1. 在该接口的主体内添加 @Insert 注解。在 @Insert 下,添加一个 insert() 函数,该函数将 Entityitem 的实例作为其参数。数据库操作的执行可能用时较长,因此,应该在单独的线程上运行这些操作。将该函数设为挂起函数,以便从协程调用该函数。
@Insert
suspend fun insert(item: Item)
  1. 添加参数 OnConflict 并为其赋值 OnConflictStrategy.IGNORE参数 OnConflict 用于告知 Room 在发生冲突时应该执行的操作。OnConflictStrategy.IGNORE 策略会忽略主键已存在于数据库中的新商品。如需详细了解可用的冲突策略,请参阅相关文档
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)

现在,Room 将生成在数据库中插入 item 所需的全部代码。当您从 Kotlin 代码调用 insert() 时,Room 将执行 SQL 查询以将该实体插入到数据库中。(注意:您可以为该函数指定任意名称;并不一定要将其命名为 insert()。)

  1. 添加 @Update 注解以及将一个 item 作为参数的 update() 函数。更新的实体与传入的实体具有相同的键。您可以更新该实体的部分或全部其他属性。与 insert() 方法类似,将以下 update() 方法设为 suspend
@Update
suspend fun update(item: Item)
  1. 添加 @Delete 注解以及用于删除商品的 delete() 函数。将其设为挂起方法。@Delete 注解会删除一个商品或一个商品列表。(注意:您需要传递要删除的实体,如果您没有该实体,可能需要在调用 delete() 函数之前获取该实体。)
@Delete
suspend fun delete(item: Item)

其余功能没有方便使用的注解,因此您必须使用 @Query 注解并提供 SQLite 查询。

  1. 编写一个 SQLite 查询,根据给定 id 从 item 表中检索特定商品。然后,您要添加 Room 注解,并在后续步骤中使用以下查询的修改版。在接下来几步中,您还要使用 Room 将其更改为 DAO 方法。
  2. 选择 item 中的所有列
  3. WHERE 语句中的 id 匹配一个具体值。

示例

SELECT * from item WHERE id = 1
  1. 更改上述 SQL 查询,以便与 Room 注解和参数配合使用。添加 @Query 注解,并将该查询作为字符串参数提供给 @Query 注解。向 @Query 添加一个 String 参数,它是用于从 item 表中检索商品的 SQLite 查询。
  2. 选择 item 中的所有列
  3. WHERE 语句中的 id 匹配 :id 参数。请注意 :id。在查询中使用英文冒号是为了引用该函数中的参数。
@Query("SELECT * from item WHERE id = :id")
  1. @Query 注解下,添加 getItem() 函数,该函数接受一个 Int 参数并返回 Flow<Item>
@Query("SELECT * from item WHERE id = :id")
fun getItem(id: Int): Flow<Item>

使用 FlowLiveData 作为返回值类型可确保您在每次数据库中的数据发生更改时收到通知。建议在持久性层中使用 FlowRoom 会为您保持更新此 Flow,也就是说,您只需要显式获取一次数据。这一点对于更新您将在下一个 Codelab 中实现的商品目录很有帮助。由于返回值类型为 Flow,Room 还会在后台线程上运行该查询。您无需将其明确设为 suspend 函数并在协程作用域内进行调用。

您可能需要从 kotlinx.coroutines.flow.Flow 导入 Flow

  1. 添加 @Query 注解和一个 getItems() 函数:
  2. 让 SQLite 查询返回 item 表中的所有列,依升序排序。
  3. getItems() 返回 Item 实体的列表作为 FlowRoom 会为您保持更新此 Flow,也就是说,您只需要显式获取一次数据。
@Query("SELECT * from item ORDER BY name ASC")
fun getItems(): Flow<List<Item>>
  1. 尽管您不会看到任何明显的更改,但您仍应运行应用以确保其没有错误。

7. 创建数据库实例

在此任务中,您将创建一个 RoomDatabase,它将使用您在上一个任务中创建的 Entity 和 DAO。该数据库类用于定义实体和数据访问对象的列表。它也是底层连接的主要访问点。

Database 类为应用提供您已定义的 DAO 实例。反过来,应用可以使用 DAO 从数据库中检索数据,作为关联的数据实体对象的实例。此外,应用还可以使用定义的数据实体更新相应表中的行,或者创建新行供插入。

您需要创建一个抽象 RoomDatabase 类,并为其添加 @Database 注解。该类有一个方法,用于创建 RoomDatabase 的实例(如果该实例尚不存在)或者返回 RoomDatabase 的现有实例。

以下是获取 RoomDatabase 实例的一般过程:

  • 创建一个扩展 RoomDatabasepublic abstract 类。您定义的新抽象类将用作数据库持有者。您定义的类是抽象类,因为 Room 会为您创建实现。
  • 为该类添加 @Database 注解。在参数中,为数据库列出实体并设置版本号。
  • 定义一个返回 ItemDao 实例的抽象方法或属性,Room 会为您生成实现。
  • 整个应用只需要一个 RoomDatabase 实例,因此请将 RoomDatabase 设为单例。
  • 使用 RoomRoom.databaseBuilder 创建 (item_database) 数据库。不过,仅当该数据库不存在时才应创建。否则,请返回现有数据库。

创建数据库

  1. data 软件包中,创建一个 Kotlin 类 ItemRoomDatabase.kt
  2. ItemRoomDatabase.kt 文件中,将 ItemRoomDatabase 类设为扩展 RoomDatabaseabstract 类。为该类添加 @Database 注解。您将在下一步中修复缺少参数的错误。
@Database
abstract class ItemRoomDatabase : RoomDatabase() {}
  1. @Database 注解需要包含几个参数,Room 才能构建数据库。
  • Item 指定为包含 entities 列表的唯一类。
  • version 设为 1。每当您更改数据库表的架构时,都必须提升版本号。
  • exportSchema 设为 false,这样就不会保留架构版本记录的备份。
@Database(entities = [Item::class], version = 1, exportSchema = false)
  1. 该数据库需要知悉 DAO。在类的主体内,声明一个返回 ItemDao 的抽象函数。您可能有多个 DAO。
abstract fun itemDao(): ItemDao
  1. 在该抽象函数下,定义一个 companion 对象。通过该伴生对象可以访问相关方法,使用类名称作为限定符来创建或获取数据库。
 companion object {}
  1. companion 对象内,为数据库声明一个私有的可为 null 变量 INSTANCE,并将其初始化为 nullINSTANCE 变量将在数据库创建后保留对数据库的引用。这有助于保持在任意时间点都只有一个打开的数据库实例,因为这种资源的创建和维护成本极高。

INSTANCE 添加 @Volatile 注解。volatile 变量的值绝不会缓存,所有读写操作都将在主内存中完成。这有助于确保 INSTANCE 的值始终是最新的值,并且对所有执行线程都相同。也就是说,一个线程对 INSTANCE 所做的更改会立即对所有其他线程可见。

@Volatile
private var INSTANCE: ItemRoomDatabase? = null
  1. INSTANCE 下但仍在 companion 对象内,定义 getDatabase() 方法并提供数据库构建器所需的 Context 参数。返回类型 ItemRoomDatabase。您将看到一条错误,因为 getDatabase() 尚不会返回任何内容。
fun getDatabase(context: Context): ItemRoomDatabase {}
  1. 多个线程有可能会遇到竞态条件并同时请求数据库实例,导致产生两个数据库而不是一个。封装代码以在 synchronized 块内获取数据库意味着一次只有一个执行线程可以进入此代码块,从而确保数据库仅初始化一次。

getDatabase() 内,返回 INSTANCE 变量;如果 INSTANCE 为 null 值,请在一个 synchronized{} 块内对其进行初始化。请使用 elvis 运算符 (?:) 执行此操作。将要锁定在函数块中的伴生对象传入 this 中。您将在后续步骤中修复该错误。

return INSTANCE ?: synchronized(this) { }
  1. 在 synchronized 块内,创建一个 val 实例变量,并使用数据库构建器获取数据库。您仍然会遇到错误,并将在后续步骤中进行修复。
val instance = Room.databaseBuilder()
  1. synchronized 块的末尾,返回 instance
return instance
  1. synchronized 块内,初始化 instance 变量,并使用数据库构建器获取数据库。将应用上下文、数据库类和数据库的名称 item_database 传入 Room.databaseBuilder() 中。
val instance = Room.databaseBuilder(
   context.applicationContext,
   ItemRoomDatabase::class.java,
   "item_database"
)

Android Studio 将生成类型不匹配错误。如需消除此错误,您必须在后续步骤中添加迁移策略和 build()

  1. 将所需的迁移策略添加到构建器中。使用 .fallbackToDestructiveMigration()

通常,您必须为迁移对象提供在架构发生更改时使用的迁移策略。迁移对象是发挥以下作用的对象:定义如何获取旧架构的所有行并将其转换为新架构中的行,使数据不会丢失。迁移不在此 Codelab 的范围内。一种简单的解决办法是销毁并重新构建数据库,这意味着数据会丢失。

.fallbackToDestructiveMigration()
  1. 如需创建数据库实例,请调用 .build()。这样做应该就会消除 Android Studio 错误。
.build()
  1. synchronized 块内,指定 INSTANCE = instance
INSTANCE = instance
  1. synchronized 块的末尾,返回 instance。您的最终代码应如下所示:
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class ItemRoomDatabase : RoomDatabase() {

   abstract fun itemDao(): ItemDao

   companion object {
       @Volatile
       private var INSTANCE: ItemRoomDatabase? = null
       fun getDatabase(context: Context): ItemRoomDatabase {
           return INSTANCE ?: synchronized(this) {
               val instance = Room.databaseBuilder(
                   context.applicationContext,
                   ItemRoomDatabase::class.java,
                   "item_database"
               )
                   .fallbackToDestructiveMigration()
                   .build()
               INSTANCE = instance
               return instance
           }
       }
   }
}
  1. 构建代码以确保没有错误。

实现应用类

在此任务中,您将在应用类中实例化数据库实例。

  1. 打开 InventoryApplication.kt,创建一个名为 database 且类型为 ItemRoomDatabaseval。通过对 ItemRoomDatabase 调用 getDatabase() 并传入上下文来实例化 database 实例。使用 lazy 委托,让实例 database 的创建延迟到您首次需要/访问引用时(而不是在应用启动时创建)。这样将在首次访问时创建数据库(磁盘上的物理数据库)。
import android.app.Application
import com.example.inventory.data.ItemRoomDatabase

class InventoryApplication : Application(){
   val database: ItemRoomDatabase by lazy { ItemRoomDatabase.getDatabase(this) }
}

稍后在此 Codelab 中,当您创建 ViewModel 实例时,您将使用此 database 实例。

现在,您已拥有使用 Room 所需的全部构建基块。该代码会编译并运行,但您无法判断它是否确实能正常运行。因此,不妨在此时向商品目录数据库中添加新商品,对数据库进行测试。为此,您需要一个 ViewModel 才能与数据库通信。

8. 添加 ViewModel

到目前为止,您已经创建了一个数据库,而界面类是起始代码的一部分。为了保存应用的瞬态数据同时也为了访问数据库,您需要一个 ViewModel。Inventory ViewModel 将通过 DAO 与数据库交互,并向界面提供数据。所有数据库操作都必须在主界面线程之外运行,您将使用协程和 viewModelScope 做到这一点。

91298a7c05e4f5e0.png

创建 Inventory ViewModel

  1. com.example.inventory 软件包中,创建一个 Kotlin 类文件 InventoryViewModel.kt
  2. ViewModel 类扩展 InventoryViewModel 类。将 ItemDao 对象作为参数传入默认构造函数。
class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {}
  1. InventoryViewModel.kt 文件末尾的类之外,添加 InventoryViewModelFactory 类以实例化 InventoryViewModel 实例。传入与 InventoryViewModel 相同的构造函数参数,即 ItemDao 实例。从 ViewModelProvider.Factory 类扩展该类。您将在下一步中修复有关未实现的方法的错误。
class InventoryViewModelFactory(private val itemDao: ItemDao) : ViewModelProvider.Factory {
}
  1. 点击红色灯泡并选择 Implement Members,或者如下替换 ViewModelProvider.Factory 类中的 create() 方法,该方法接受任何类类型作为参数并返回一个 ViewModel 对象。
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   TODO("Not yet implemented")
}
  1. 实现 create() 方法。检查 modelClass 是否与 InventoryViewModel 类相同并返回其实例。如果不是,就抛出异常。
if (modelClass.isAssignableFrom(InventoryViewModel::class.java)) {
   @Suppress("UNCHECKED_CAST")
   return InventoryViewModel(itemDao) as T
}
throw IllegalArgumentException("Unknown ViewModel class")

填充 ViewModel

在此任务中,您将填充 InventoryViewModel 类以将商品目录数据添加到数据库中。观察 Item 实体和 Inventory 应用中的 Add Item 屏幕。

@Entity
data class Item(
   @PrimaryKey(autoGenerate = true)
   val id: Int = 0,
   @ColumnInfo(name = "name")
   val itemName: String,
   @ColumnInfo(name = "price")
   val itemPrice: Double,
   @ColumnInfo(name = "quantity")
   val quantityInStock: Int
)

85c644aced4198c5.png

为了将某个实体添加到数据库中,您需要有特定商品的名称、价格和现货库存信息。稍后在此 Codelab 中,您将使用 Add Item 屏幕从用户那里获取这些详细信息。在当前任务中,您要使用三个字符串作为 ViewModel 的输入,将其转换为一个 Item 实体实例,并使用 ItemDao 实例将该实体实例保存到数据库中。现在开始实现吧。

  1. InventoryViewModel 类中,添加一个名为 insertItem() 且接受 Item 对象的 private 函数,并以非阻塞方式将数据添加到数据库中。
private fun insertItem(item: Item) {
}
  1. 如需在主线程之外与数据库交互,请启动协程并在其中调用 DAO 方法。在 insertItem() 方法内,使用 viewModelScope.launchViewModelScope 中启动协程。在 launch 函数内,对 itemDao 调用挂起函数 insert(),并传入 itemViewModelScopeViewModel 类的扩展属性,用于在 ViewModel 被销毁时自动取消其子协程。
private fun insertItem(item: Item) {
   viewModelScope.launch {
       itemDao.insert(item)
   }
}

导入 kotlinx.coroutines.launch,androidx.lifecycle.viewModelScope

com.example.inventory.data.Item(如果未自动导入)。

  1. InventoryViewModel 类中,添加另一个私有函数,该函数接受三个字符串并返回一个 Item 实例。
private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item {
   return Item(
       itemName = itemName,
       itemPrice = itemPrice.toDouble(),
       quantityInStock = itemCount.toInt()
   )
}
  1. 仍是在 InventoryViewModel 类中,添加一个名为 addNewItem() 的公共函数,该函数接受三个商品详情字符串。将商品详情字符串传入 getNewItemEntry() 函数,并将返回的值赋值给名为 newItem 的值。调用 insertItem(),并传入 newItem 将新实体添加到数据库中。此调用将从界面 fragment 调用,以将商品详情添加到数据库中。
fun addNewItem(itemName: String, itemPrice: String, itemCount: String) {
   val newItem = getNewItemEntry(itemName, itemPrice, itemCount)
   insertItem(newItem)
}

请注意,您没有对 addNewItem() 使用 viewModelScope.launch,但在前面的 insertItem() 中,调用 DAO 方法时却需要使用该函数。其原因在于,挂起函数只能从协程或其他挂起函数中调用。函数 itemDao.insert(item) 就是一个挂起函数。

向数据库添加实体所需的函数已全部添加。在下一个任务中,您将更新 Add Item fragment 以使用上述函数。

9. 更新 AddItemFragment

  1. AddItemFragment.kt 中的 AddItemFragment 类的开头,创建一个名为 viewModel 且类型为 InventoryViewModelprivate val。使用 by activityViewModels() Kotlin 属性委托在 fragment 之间共享 ViewModel。您将在下一步中修复该错误。
private val viewModel: InventoryViewModel by activityViewModels {
}
  1. 在 lambda 内,调用 InventoryViewModelFactory() 构造函数并传入 ItemDao 实例。使用您在前面的一个任务中创建的 database 实例调用 itemDao 构造函数。
private val viewModel: InventoryViewModel by activityViewModels {
   InventoryViewModelFactory(
       (activity?.application as InventoryApplication).database
           .itemDao()
   )
}
  1. viewModel 定义下,创建一个名为 item 且类型为 Itemlateinit var
 lateinit var item: Item
  1. Add Item 屏幕包含三个文本字段,用于从用户那里获取商品详情。在此步骤中,您将添加一个函数来验证这些文本字段中的文本是否不为空。在数据库中添加或更新实体之前,您将使用此函数验证用户输入。此验证需要在 ViewModel 中完成,而不是在 fragment 中完成。在 InventoryViewModel 类中,添加以下名为 isEntryValid()public 函数。
fun isEntryValid(itemName: String, itemPrice: String, itemCount: String): Boolean {
   if (itemName.isBlank() || itemPrice.isBlank() || itemCount.isBlank()) {
       return false
   }
   return true
}
  1. AddItemFragment.kt 中的 onCreateView() 函数下,创建一个名为 isEntryValid()private 函数,该函数将返回一个 Boolean。您将在下一步中修复缺少返回值的错误。
private fun isEntryValid(): Boolean {
}
  1. AddItemFragment 类中,实现 isEntryValid() 函数。对 viewModel 实例调用 isEntryValid() 函数,并传入文本视图中的文本。返回 viewModel.isEntryValid() 函数的值。
private fun isEntryValid(): Boolean {
   return viewModel.isEntryValid(
       binding.itemName.text.toString(),
       binding.itemPrice.text.toString(),
       binding.itemCount.text.toString()
   )
}
  1. AddItemFragment 类中的 isEntryValid() 函数下,添加名为 addNewItem() 另一个 private 函数,该函数不带参数也不返回任何内容。在该函数内的 if 条件内调用 isEntryValid()
private fun addNewItem() {
   if (isEntryValid()) {
   }
}
  1. if 块内,对 viewModel 实例调用 addNewItem() 方法。传递用户输入的商品详情,并使用 binding 实例进行读取。
if (isEntryValid()) {
   viewModel.addNewItem(
   binding.itemName.text.toString(),
   binding.itemPrice.text.toString(),
   binding.itemCount.text.toString(),
   )
}
  1. if 块下,创建一个 val action,用于返回到 ItemListFragment。调用 findNavController().navigate(),并传入 action
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)

导入 androidx.navigation.fragment.findNavController.

  1. 完整的方法应如下所示。
private fun addNewItem() {
       if (isEntryValid()) {
           viewModel.addNewItem(
               binding.itemName.text.toString(),
               binding.itemPrice.text.toString(),
               binding.itemCount.text.toString(),
           )
           val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
           findNavController().navigate(action)
       }
}
  1. 为了将一切连接到一起,请为 Save 按钮添加一个点击处理程序。在 AddItemFragment 类中的 onDestroyView() 函数上方,替换 onViewCreated() 函数。
  2. onViewCreated() 函数内,为 Save 按钮添加一个点击处理程序,并在其中调用 addNewItem()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
   binding.saveAction.setOnClickListener {
       addNewItem()
   }
}
  1. 构建并运行您的应用。点按 + FAB。在 Add Item 屏幕中,添加商品详情并点按 Save。此操作会保存数据,但您尚无法在应用中看到任何内容。在下一个任务中,您将使用 Database Inspector 查看您保存的数据。

193c7fa9c41e0819.png

使用 Database Inspector 查看数据库

  1. 在搭载 API 级别 26 或更高版本的模拟器或已连接设备上运行您的应用(如果您尚未这样做)。Database Inspector 在搭载 API 级别 26 的模拟器/设备上使用效果最佳。
  2. 在 Android Studio 中,从菜单栏中依次选择 View > Tool Windows > Database Inspector
  3. 在 Database Inspector 窗格中,从下拉菜单中选择 com.example.inventory
  4. Inventory 应用中的 item_database 将显示于 Databases 窗格中。展开 item_database 节点,然后选择 Item 进行检查。如果 Databases 窗格为空,请使用模拟器通过 Add Item 屏幕向数据库中添加一些商品。
  5. 选中 Database Inspector 中的 Live updates 复选框,以便随着您与模拟器或设备中正在运行的应用互动而自动更新呈现的数据。

4803c08f94e34118.png

恭喜!您已创建了一个可以使用 Room 持久保留数据的应用。在下一个 Codelab 中,您将向应用中添加一个 RecyclerView 以显示数据库中的项,并向应用中添加删除和更新实体等新功能。到时候见!

10. 解决方案代码

此 Codelab 的解决方案代码位于下方所示的 GitHub 代码库和分支中。

如需获取此 Codelab 的代码并在 Android Studio 中打开它,请执行以下操作。

获取代码

  1. 点击提供的网址。此时,项目的 GitHub 页面会在浏览器中打开。
  2. 在项目的 GitHub 页面上,点击 Code 按钮,这时会出现一个对话框。

5b0a76c50478a73f.png

  1. 在对话框中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
  2. 在计算机上找到该文件(可能在 Downloads 文件夹中)。
  3. 双击 ZIP 文件进行解压缩。系统将创建一个包含项目文件的新文件夹。

在 Android Studio 中打开项目

  1. 启动 Android Studio。
  2. Welcome to Android Studio 窗口中,点击 Open an existing Android Studio project

36cc44fcf0f89a1d.png

注意:如果 Android Studio 已经打开,请依次选择 File > New > Import Project 菜单选项。

21f3eec988dcfbe9.png

  1. Import Project 对话框中,找到解压缩的项目文件夹所在的位置(可能在 Downloads 文件夹中)。
  2. 双击该项目文件夹。
  3. 等待 Android Studio 打开项目。
  4. 点击 Run 按钮 11c34fc5e516fb1c.png 以构建并运行应用。请确保该应用按预期构建。
  5. Project 工具窗口中浏览项目文件,了解应用的设置方式。

11. 总结

  • 将您的表定义为带有 @Entity 注解的数据类。将带有 @ColumnInfo 注解的属性定义为表中的列。
  • 将数据访问对象 (DAO) 定义为带有 @Dao 注解的接口。DAO 用于将 Kotlin 函数映射到数据库查询。
  • 使用注解来定义 @Insert@Delete@Update 函数。
  • @Query 注解和作为参数的 SQLite 查询字符串用于所有其他查询。
  • 使用 Database Inspector 查看 Android SQLite 数据库中保存的数据。

12. 了解更多内容

Android 开发者文档

博文

视频

其他文档和文章