使用Room持久保留數(shù)據(jù)

大多數(shù)達(dá)到生產(chǎn)質(zhì)量標(biāo)準(zhǔn)的應(yīng)用都包含需要持久保留的數(shù)據(jù)玫膀。例如,應(yīng)用可能會(huì)存儲(chǔ)歌曲播放列表爹脾、待辦事項(xiàng)列表中的內(nèi)容帖旨、支出和收入記錄、星座目錄或個(gè)人數(shù)據(jù)歷史記錄灵妨。對(duì)于此類(lèi)用例解阅,您可以使用數(shù)據(jù)庫(kù)來(lái)存儲(chǔ)這些持久性數(shù)據(jù)。

Room 是一個(gè)持久性庫(kù)泌霍,屬于 Android Jetpack 的一部分瓮钥。Room 是在 SQLite 數(shù)據(jù)庫(kù)基礎(chǔ)上構(gòu)建的一個(gè)抽象層。SQLite 使用一種專(zhuān)門(mén)的語(yǔ)言 (SQL) 來(lái)執(zhí)行數(shù)據(jù)庫(kù)操作烹吵。Room 并不直接使用 SQLite,而是負(fù)責(zé)簡(jiǎn)化數(shù)據(jù)庫(kù)設(shè)置和配置以及數(shù)據(jù)庫(kù)與應(yīng)用交互方面的瑣碎工作桨武。Room 還提供 SQLite 語(yǔ)句的編譯時(shí)檢查肋拔。

抽象層是一組隱藏了底層實(shí)現(xiàn)/復(fù)雜性的函數(shù)。抽象層可為現(xiàn)有功能集提供一個(gè)接口呀酸,就像在本例中使用 SQLite 一樣凉蜂。

下圖展示了 Room 作為數(shù)據(jù)源如何融入本課程中推薦的總體架構(gòu)。Room 是一個(gè)數(shù)據(jù)源性誉。

image.png

前提條件

  • 能夠使用 Jetpack Compose 為 Android 應(yīng)用構(gòu)建基本界面窿吩。

  • 能夠使用 TextIcon错览、IconButtonLazyColumn 等可組合函數(shù)纫雁。

  • 能夠使用 NavHost 可組合函數(shù)定義應(yīng)用中的路線和界面。

  • 能夠使用 NavHostController 在界面之間導(dǎo)航倾哺。

  • 熟悉 Android 架構(gòu)組件 ViewModel轧邪。能夠使用 ViewModelProvider.Factory 實(shí)例化 ViewModel刽脖。

  • 熟悉并發(fā)基礎(chǔ)知識(shí)。

  • 能夠使用協(xié)程管理長(zhǎng)時(shí)間運(yùn)行的任務(wù)忌愚。

  • 掌握 SQLite 數(shù)據(jù)庫(kù)和 SQL 語(yǔ)言的基礎(chǔ)知識(shí)曲管。

演示應(yīng)用概覽

在此 演示應(yīng)用中,將使用 Inventory 應(yīng)用的起始代碼硕糊,并使用 Room 庫(kù)向其中添加數(shù)據(jù)庫(kù)層院水。最終版本的應(yīng)用會(huì)顯示商品目錄數(shù)據(jù)庫(kù)中的商品列表。用戶可以選擇在商品目錄數(shù)據(jù)庫(kù)中添加新商品简十、更新現(xiàn)有商品和刪除其中的商品檬某。在此 演示中,需要將商品數(shù)據(jù)保存到 Room 數(shù)據(jù)庫(kù)勺远。

顯示商品目錄中商品的手機(jī)屏幕
手機(jī)屏幕中顯示“Add item”界面橙喘。
已填寫(xiě)商品詳情的手機(jī)屏幕。

注意:以上屏幕截圖來(lái)自本在線課程結(jié)束時(shí)的最終版應(yīng)用胶逢,而不是此 Codelab 結(jié)束時(shí)的應(yīng)用厅瞎。這些屏幕截圖旨在讓您對(duì)該應(yīng)用的最終版本有一個(gè)大致的概念。

下面請(qǐng)下載起始代碼:

file_download下載 ZIP 文件

或者初坠,也可以克隆該代碼的 GitHub 代碼庫(kù):

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-inventory-app.git
$ cd basic-android-kotlin-compose-training-inventory-app
$ git checkout starter

注意:起始代碼位于所下載代碼庫(kù)的 starter 分支中和簸。

可以在 Inventory app GitHub 代碼庫(kù)中瀏覽該代碼。

起始代碼概覽

  1. 在 Android Studio 中打開(kāi)包含起始代碼的項(xiàng)目碟刺。

  2. 在 Android 設(shè)備或模擬器上運(yùn)行應(yīng)用锁保。確保模擬器或連接的設(shè)備搭載的是 API 級(jí)別 26 或更高版本。 Database Inspector 適用于搭載 API 級(jí)別 26 及更高版本的模擬器/設(shè)備半沽。

注意:借助 Database Inspector爽柒,您可以在應(yīng)用運(yùn)行時(shí)檢查、查詢和修改應(yīng)用的數(shù)據(jù)庫(kù)者填。Database Inspector 可處理普通的 SQLite 數(shù)據(jù)庫(kù)或在 SQLite 的基礎(chǔ)上構(gòu)建的庫(kù)(例如 Room)浩村。

  1. 該應(yīng)用未顯示任何商品目錄數(shù)據(jù)。

  2. 點(diǎn)按懸浮操作按鈕 (FAB) 向數(shù)據(jù)庫(kù)中添加新商品占哟。

應(yīng)用會(huì)轉(zhuǎn)到一個(gè)新界面心墅,可以在其中輸入新商品的詳情。

顯示空白商品目錄的手機(jī)屏幕
手機(jī)屏幕中顯示“Add item”界面

起始代碼存在的問(wèn)題

  1. Add Item 界面中榨乎,輸入商品的詳情怎燥,例如名稱(chēng)、價(jià)格和數(shù)量蜜暑。

  2. 點(diǎn)按 Save铐姚。Add Item 界面未關(guān)閉,但您可以使用返回鍵返回史煎。保存功能未實(shí)現(xiàn)谦屑,因此系統(tǒng)不會(huì)保存商品詳情驳糯。

請(qǐng)注意,該應(yīng)用尚未完成氢橙,Save 按鈕功能尚未實(shí)現(xiàn)酝枢。

已填寫(xiě)商品詳情的手機(jī)屏幕。

在此 Codelab 中悍手,您將添加使用 Room 將商品目錄詳情保存到 SQLite 數(shù)據(jù)庫(kù)中的代碼帘睦。您可以使用 Room 持久性庫(kù)與 SQLite 數(shù)據(jù)庫(kù)進(jìn)行交互。

代碼演示

下載的起始代碼已為您預(yù)先設(shè)計(jì)了界面布局坦康。只需專(zhuān)心實(shí)現(xiàn)數(shù)據(jù)庫(kù)邏輯即可竣付。以下部分簡(jiǎn)要介紹了一些文件。

ui/home/HomeScreen.kt

此文件是主屏幕滞欠,即應(yīng)用的第一個(gè)屏幕古胆,其中包含用于顯示商品目錄列表的可組合函數(shù)。它包含一個(gè) FAB [圖片上傳失敗...(image-f8c2cf-1712916361380)] 筛璧,可用于向列表中添加新商品逸绎。

顯示商品目錄中商品的手機(jī)屏幕

ui/item/ItemEntryScreen.kt

此界面類(lèi)似于 ItemEditScreen.kt。它們都提供了用于輸入商品詳情的文本字段夭谤。點(diǎn)按主屏幕中的 FAB 即會(huì)顯示此界面棺牧。ItemEntryViewModel.kt 是此界面的對(duì)應(yīng) ViewModel

已填寫(xiě)商品詳情的手機(jī)屏幕

ui/navigation/InventoryNavGraph.kt

Room 的主要組件

Kotlin 提供了一種通過(guò)數(shù)據(jù)類(lèi)輕松處理數(shù)據(jù)的方式朗儒。雖然使用數(shù)據(jù)類(lèi)可以輕松地處理內(nèi)存中的數(shù)據(jù)颊乘,但當(dāng)需要持久保留數(shù)據(jù)時(shí),就需要將這些數(shù)據(jù)轉(zhuǎn)換為與數(shù)據(jù)庫(kù)存儲(chǔ)系統(tǒng)兼容的格式醉锄。為此乏悄,可以使用表來(lái)存儲(chǔ)數(shù)據(jù),并使用查詢來(lái)訪問(wèn)和修改數(shù)據(jù)恳不。

Room 的以下三個(gè)組件可以使這些工作流變得順暢纲爸。

  • Room 實(shí)體表示應(yīng)用數(shù)據(jù)庫(kù)中的表∽惫唬可以使用它們更新表中的行所存儲(chǔ)的數(shù)據(jù),以及創(chuàng)建要插入的新行负蚊。

  • Room DAO 提供了供應(yīng)用在數(shù)據(jù)庫(kù)中檢索神妹、更新、插入和刪除數(shù)據(jù)的方法家妆。

  • Room Database 類(lèi)是一個(gè)數(shù)據(jù)庫(kù)類(lèi)鸵荠,可為應(yīng)用提供與該數(shù)據(jù)庫(kù)關(guān)聯(lián)的 DAO 實(shí)例。

下圖演示了 Room 的各組件如何協(xié)同工作以與數(shù)據(jù)庫(kù)交互伤极。

演示 Room 數(shù)據(jù)訪問(wèn)對(duì)象和實(shí)體如何與應(yīng)用其余部分交互的圖表

添加 Room 依賴項(xiàng)

向 Gradle 文件添加所需的 Room 組件庫(kù)蛹找。

  1. 打開(kāi)模塊級(jí) Gradle 文件 build.gradle.kts (Module: InventoryApp.app)姨伤。

  2. dependencies 代碼塊中郊闯,為 Room 庫(kù)添加依賴項(xiàng)悦穿,如以下代碼所示。

//Room
implementation("androidx.room:room-runtime:${rootProject.extra["room_version"]}")
ksp("androidx.room:room-compiler:${rootProject.extra["room_version"]}")
implementation("androidx.room:room-ktx:${rootProject.extra["room_version"]}")

KSP 是一個(gè)功能強(qiáng)大且簡(jiǎn)單易用的 API丛楚,用于解析 Kotlin 注解届慈。

?注意:對(duì)于 Gradle 文件中的庫(kù)依賴項(xiàng)徒溪,請(qǐng)務(wù)必使用 AndroidX 版本頁(yè)面中最新穩(wěn)定發(fā)布版本的版本號(hào)。

創(chuàng)建 item 實(shí)體

Entity 類(lèi)定義了一個(gè)表金顿,該類(lèi)的每個(gè)實(shí)例都表示數(shù)據(jù)庫(kù)表中的一行臊泌。Entity 類(lèi)以映射告知 Room 它打算如何呈現(xiàn)數(shù)據(jù)庫(kù)中的信息并與之交互。在演示的應(yīng)用中揍拆,實(shí)體將保存有關(guān)商品目錄商品的信息渠概,例如商品名稱(chēng)、商品價(jià)格和商品數(shù)量嫂拴。

顯示實(shí)體字段和實(shí)體實(shí)例的表格

@Entity 注解用于將某個(gè)類(lèi)標(biāo)記為數(shù)據(jù)庫(kù) Entity 類(lèi)播揪。對(duì)于每個(gè) Entity 類(lèi),該應(yīng)用都會(huì)創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)表來(lái)保存這些項(xiàng)顷牌。除非另行說(shuō)明剪芍,否則 Entity 的每個(gè)字段在數(shù)據(jù)庫(kù)中都表示為一列(如需了解詳情,請(qǐng)參閱實(shí)體文檔)窟蓝。存儲(chǔ)在數(shù)據(jù)庫(kù)中的每個(gè)實(shí)體實(shí)例都必須有一個(gè)主鍵罪裹。主鍵用于唯一標(biāo)識(shí)數(shù)據(jù)庫(kù)表中的每個(gè)記錄/條目。應(yīng)用分配主鍵后运挫,便無(wú)法再修改主鍵状共;只要主鍵存在于數(shù)據(jù)庫(kù)中,它就會(huì)表示實(shí)體對(duì)象谁帕。

在此演示中峡继,將創(chuàng)建一個(gè) Entity 類(lèi),并定義字段來(lái)存儲(chǔ)每個(gè)商品的以下商品目錄信息:Int 用于存儲(chǔ)主鍵匈挖,String 用于存儲(chǔ)商品名稱(chēng)碾牌,double 用于存儲(chǔ)商品價(jià)格,Int 用于存儲(chǔ)庫(kù)存數(shù)量儡循。

  1. data 舶吗。

  2. data 軟件包內(nèi),新建Item Kotlin 類(lèi)择膝,該類(lèi)表示應(yīng)用中的數(shù)據(jù)庫(kù)實(shí)體誓琼。

// No need to copy over, this is part of the starter code
class Item(
    val id: Int,
    val name: String,
    val price: Double,
    val quantity: Int
)

?注意:主要構(gòu)造函數(shù)是 Kotlin 類(lèi)中的類(lèi)標(biāo)頭的一部分,它跟在類(lèi)名稱(chēng)(以及可選的類(lèi)型參數(shù))之后。

數(shù)據(jù)類(lèi)

數(shù)據(jù)類(lèi)在 Kotlin 中主要用于保存數(shù)據(jù)腹侣。它們使用關(guān)鍵字 data 進(jìn)行定義叔收。Kotlin 數(shù)據(jù)類(lèi)對(duì)象有一些額外的優(yōu)勢(shì)。例如傲隶,編譯器會(huì)自動(dòng)生成用于比較饺律、輸出和復(fù)制的實(shí)用程序,如 toString()伦籍、copy()equals()蓝晒。

示例:

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

為了確保生成的代碼的一致性,也為了確保其行為有意義帖鸦,數(shù)據(jù)類(lèi)必須滿足以下要求:

  • 主要構(gòu)造函數(shù)必須至少有一個(gè)參數(shù)芝薇。

  • 所有主要構(gòu)造函數(shù)參數(shù)都必須是 valvar

  • 數(shù)據(jù)類(lèi)不能為 abstract作儿、opensealed洛二。

? 警告:編譯器只會(huì)將主構(gòu)造函數(shù)內(nèi)定義的屬性用于自動(dòng)生成的函數(shù)。編譯器會(huì)從生成的實(shí)現(xiàn)中排除類(lèi)主體中聲明的屬性攻锰。

如需詳細(xì)了解數(shù)據(jù)類(lèi)晾嘶,請(qǐng)參閱數(shù)據(jù)類(lèi)文檔。

  1. Item 類(lèi)的定義添加前綴 data 關(guān)鍵字娶吞,以將其轉(zhuǎn)換為數(shù)據(jù)類(lèi)垒迂。
data class Item(
    val id: Int,
    val name: String,
    val price: Double,
    val quantity: Int
)
  1. Item 類(lèi)聲明的上方,為該數(shù)據(jù)類(lèi)添加 @Entity 注解妒蛇。使用 tableName 參數(shù)將 items 設(shè)置為 SQLite 表名稱(chēng)机断。
import androidx.room.Entity

@Entity(tableName = "items")
data class Item(
   ...
)

?注意@Entity 注解有多個(gè)可能的參數(shù)。默認(rèn)情況下(@Entity 沒(méi)有參數(shù))绣夺,表名稱(chēng)與類(lèi)名稱(chēng)相同吏奸。使用 tableName 參數(shù)可自定義表名稱(chēng)。為簡(jiǎn)單起見(jiàn)陶耍,請(qǐng)使用 item奋蔚。@Entity 還有幾個(gè)其他參數(shù),您可以參閱實(shí)體文檔進(jìn)行研究烈钞。

  1. id 屬性添加 @PrimaryKey 注解泊碑,使 id 成為主鍵。主鍵是一個(gè) ID毯欣,用于唯一標(biāo)識(shí) Item 表格中的每個(gè)記錄/條目
import androidx.room.PrimaryKey

@Entity(tableName = "items")
data class Item(
    @PrimaryKey
    val id: Int,
    ...
)
  1. id 分配默認(rèn)值 0蛾狗,這樣才能使 id 自動(dòng)生成 id 值。

  2. 將參數(shù) autoGenerate 設(shè)為 true仪媒,以便 Room 為每個(gè)實(shí)體生成一個(gè)遞增 ID。這樣做可以保證每個(gè)商品的 ID 都是唯一的。

data class Item(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    // ...
)

創(chuàng)建 item DAO

數(shù)據(jù)訪問(wèn)對(duì)象 (DAO) 是一種模式算吩,其作用是通過(guò)提供抽象接口將持久性數(shù)據(jù)層與應(yīng)用的其余部分分離留凭。這種分離遵循單一責(zé)任原則

DAO 的功能在于偎巢,讓在底層持久性數(shù)據(jù)層執(zhí)行數(shù)據(jù)庫(kù)操作所涉及的所有復(fù)雜性與應(yīng)用的其余部分分離蔼夜。這樣,就可以獨(dú)立于使用數(shù)據(jù)的代碼更改數(shù)據(jù)層压昼。

下面將為 Room 定義一個(gè) DAO求冷。DAO 是 Room 的主要組件,負(fù)責(zé)定義用于訪問(wèn)數(shù)據(jù)庫(kù)的接口窍霞。

創(chuàng)建的 DAO 是一個(gè)自定義接口匠题,提供查詢/檢索、插入但金、刪除和更新數(shù)據(jù)庫(kù)的便捷方法韭山。Room 將在編譯時(shí)生成該類(lèi)的實(shí)現(xiàn)。

Room 庫(kù)提供了便捷注解(例如 @Insert冷溃、@Delete@Update)钱磅,用于定義執(zhí)行簡(jiǎn)單插入、刪除和更新的方法似枕,而無(wú)需編寫(xiě) SQL 語(yǔ)句盖淡。

如果需要定義更復(fù)雜的插入、刪除或更新操作凿歼,或者需要查詢數(shù)據(jù)庫(kù)中的數(shù)據(jù)褪迟,請(qǐng)改用 @Query 注解。

另一個(gè)好處是毅往,當(dāng)在 Android Studio 中編寫(xiě)查詢時(shí)牵咙,編譯器會(huì)檢查 SQL 查詢是否存在語(yǔ)法錯(cuò)誤。

對(duì)于當(dāng)前演示的應(yīng)用攀唯,我們需要能夠執(zhí)行以下操作:

  • 插入或添加新商品洁桌。

  • 更新現(xiàn)有商品的名稱(chēng)、價(jià)格和數(shù)量侯嘀。

  • 根據(jù)主鍵 id 獲取特定商品另凌。

  • 獲取所有商品,從而可以顯示它們戒幔。

  • 刪除數(shù)據(jù)庫(kù)中的條目吠谢。

完成以下步驟,以在實(shí)現(xiàn)商品 DAO:

  1. data 軟件包中诗茎,創(chuàng)建 Kotlin 接口 ItemDao.kt工坊。
名稱(chēng)字段已填充為商品 DAO
  1. 為接口 ItemDao 添加 @Dao 注解。
import androidx.room.Dao

@Dao
interface ItemDao {
}
  1. 在該接口的主體內(nèi)添加 @Insert 注解。

  2. @Insert 下王污,添加一個(gè) insert() 函數(shù)罢吃,該函數(shù)將 Entity 類(lèi)的實(shí)例 item 作為其參數(shù)。

  3. 使用 suspend 關(guān)鍵字標(biāo)記函數(shù)昭齐,使其在單獨(dú)的線程上運(yùn)行尿招。

數(shù)據(jù)庫(kù)操作的執(zhí)行可能用時(shí)較長(zhǎng),因此需要在單獨(dú)的線程上運(yùn)行阱驾。Room 不允許在主線程上訪問(wèn)數(shù)據(jù)庫(kù)就谜。

import androidx.room.Insert

@Insert
suspend fun insert(item: Item)

將商品插入數(shù)據(jù)庫(kù)中時(shí),可能會(huì)發(fā)生沖突里覆。例如丧荐,代碼中的多個(gè)位置嘗試使用存在沖突的不同值(比如同一主鍵)更新實(shí)體。實(shí)體是數(shù)據(jù)庫(kù)中的行租谈。在本演示應(yīng)用中篮奄,我們僅從一處(即 Add Item 界面)插入實(shí)體,因此我們預(yù)計(jì)不會(huì)發(fā)生任何沖突割去,可以將沖突策略設(shè)為 Ignore窟却。

  1. 添加參數(shù) onConflict 并為其賦值 OnConflictStrategy.``IGNORE

參數(shù) onConflict 用于告知 Room 在發(fā)生沖突時(shí)應(yīng)該執(zhí)行的操作呻逆。OnConflictStrategy.IGNORE 策略會(huì)忽略新商品夸赫。

如需詳細(xì)了解可用的沖突策略,請(qǐng)參閱 OnConflictStrategy 文檔咖城。

import androidx.room.OnConflictStrategy

@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)

現(xiàn)在茬腿,Room 會(huì)生成將 item 插入數(shù)據(jù)庫(kù)所需的所有代碼。當(dāng)調(diào)用任何帶有 Room 注解的 DAO 函數(shù)時(shí)宜雀,Room 將在數(shù)據(jù)庫(kù)上執(zhí)行相應(yīng)的 SQL 查詢切平。例如,從 Kotlin 代碼調(diào)用上述方法 insert() 時(shí)辐董,Room 會(huì)執(zhí)行 SQL 查詢以將實(shí)體插入到數(shù)據(jù)庫(kù)中悴品。

  1. 添加一個(gè)帶有 @Update 注解的新函數(shù),該函數(shù)接受 Item 作為參數(shù)简烘。

更新的實(shí)體與傳入的實(shí)體具有相同的主鍵苔严。您可以更新該實(shí)體的部分或全部其他屬性。

  1. insert() 方法類(lèi)似孤澎,請(qǐng)使用 suspend 關(guān)鍵字標(biāo)記此函數(shù)届氢。
import androidx.room.Update

@Update
suspend fun update(item: Item)

添加另一個(gè)帶有 @Delete 注解的函數(shù)以刪除商品,并將其設(shè)為掛起函數(shù)覆旭。

注意@Delete 注解會(huì)刪除一個(gè)商品或一個(gè)商品列表退子。您需要傳遞要?jiǎng)h除的實(shí)體岖妄。如果您沒(méi)有實(shí)體,則可能需要在調(diào)用 delete() 函數(shù)之前提取該實(shí)體寂祥。

import androidx.room.Delete

@Delete
suspend fun delete(item: Item)

其余功能沒(méi)有便利注解衣吠,因此必須使用 @Query 注解并提供 SQLite 查詢。

  1. 編寫(xiě)一個(gè) SQLite 查詢壤靶,根據(jù)給定 id 從 item 表中檢索特定商品。以下代碼提供了一個(gè)示例查詢惊搏,該查詢從 items 中選擇所有列贮乳,其中 id 與特定值匹配,id 是一個(gè)唯一標(biāo)識(shí)符恬惯。

示例:

// Example, no need to copy over
SELECT * from items WHERE id = 1
  1. 添加 @Query 注解向拆。

  2. 使用上一步中的 SQLite 查詢作為 @Query 注解的字符串參數(shù)。

  3. @Query 添加一個(gè) String 參數(shù)酪耳,它是用于從 item 表中檢索商品的 SQLite 查詢浓恳。

該查詢現(xiàn)在會(huì)從 items 中選擇所有列,其中 id 與 :id 參數(shù)匹配碗暗。請(qǐng)注意颈将,:id 在查詢中使用英文冒號(hào)來(lái)引用函數(shù)中的參數(shù)。

@Query("SELECT * from items WHERE id = :id")
  1. @Query 注解后面言疗,添加一個(gè)接受 Int 參數(shù)并返回 Flow<Item>getItem() 函數(shù)晴圾。
import androidx.room.Query
import kotlinx.coroutines.flow.Flow

@Query("SELECT * from items WHERE id = :id")
fun getItem(id: Int): Flow<Item>

建議在持久性層中使用 Flow。將返回值類(lèi)型設(shè)為 Flow 后噪奄,只要數(shù)據(jù)庫(kù)中的數(shù)據(jù)發(fā)生更改死姚,您就會(huì)收到通知。Room 會(huì)為您保持更新此 Flow勤篮,也就是說(shuō)都毒,您只需要顯式獲取一次數(shù)據(jù)。此設(shè)置有助于更新您將在下一個(gè)實(shí)現(xiàn)的商品目錄碰缔。由于返回值類(lèi)型為 Flow账劲,Room 還會(huì)在后臺(tái)線程上運(yùn)行該查詢。您無(wú)需將其明確設(shè)為 suspend 函數(shù)并在協(xié)程作用域內(nèi)進(jìn)行調(diào)用手负。

  1. 添加 @Query 注解和 getAllItems() 函數(shù)涤垫。

  2. 讓 SQLite 查詢返回 item 表中的所有列,依升序排序竟终。

  3. getAllItems() 返回 Item 實(shí)體的列表作為 Flow蝠猬。Room 會(huì)為您保持更新此 Flow,也就是說(shuō)统捶,您只需要顯式獲取一次數(shù)據(jù)榆芦。

@Query("SELECT * from items ORDER BY name ASC")
fun getAllItems(): Flow<List<Item>>

已完成 ItemDao

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow

@Dao
interface ItemDao {
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(item: Item)

    @Update
    suspend fun update(item: Item)

    @Delete
    suspend fun delete(item: Item)

    @Query("SELECT * from items WHERE id = :id")
    fun getItem(id: Int): Flow<Item>

    @Query("SELECT * from items ORDER BY name ASC")
    fun getAllItems(): Flow<List<Item>>
}
  1. 盡管您不會(huì)看到任何明顯的更改柄粹,但您仍應(yīng)構(gòu)建應(yīng)用以確保其沒(méi)有錯(cuò)誤。

創(chuàng)建 Database 實(shí)例

創(chuàng)建一個(gè) RoomDatabase匆绣,它使用以上的 Entity 和 DAO驻右。數(shù)據(jù)庫(kù)類(lèi)定義了實(shí)體和 DAO 的列表。

Database 類(lèi)可為應(yīng)用提供您定義的 DAO 實(shí)例崎淳。反過(guò)來(lái)堪夭,應(yīng)用可以使用 DAO 從數(shù)據(jù)庫(kù)中檢索數(shù)據(jù),作為關(guān)聯(lián)的數(shù)據(jù)實(shí)體對(duì)象的實(shí)例拣凹。此外森爽,應(yīng)用還可以使用定義的數(shù)據(jù)實(shí)體更新相應(yīng)表中的行,或者創(chuàng)建新行供插入嚣镜。

創(chuàng)建一個(gè)抽象 RoomDatabase 類(lèi)爬迟,并為其添加 @Database 注解。此類(lèi)有一個(gè)方法菊匿,如果數(shù)據(jù)庫(kù)不存在付呕,該方法會(huì)返回 RoomDatabase 的現(xiàn)有實(shí)例。

以下是獲取 RoomDatabase 實(shí)例的一般過(guò)程:

  • 創(chuàng)建一個(gè)擴(kuò)展 RoomDatabasepublic abstract 類(lèi)跌捆。定義的新抽象類(lèi)將用作數(shù)據(jù)庫(kù)持有者徽职。定義的類(lèi)是抽象類(lèi),因?yàn)?Room 會(huì)為您創(chuàng)建實(shí)現(xiàn)疹蛉。

  • 為該類(lèi)添加 @Database 注解活箕。在參數(shù)中,為數(shù)據(jù)庫(kù)列出實(shí)體并設(shè)置版本號(hào)可款。

  • 定義一個(gè)返回 ItemDao 實(shí)例的抽象方法或?qū)傩裕?code>Room 會(huì)為您生成實(shí)現(xiàn)育韩。

  • 整個(gè)應(yīng)用只需要一個(gè) RoomDatabase 實(shí)例,因此請(qǐng)將 RoomDatabase 設(shè)為單例闺鲸。

  • 使用 RoomRoom.databaseBuilder 創(chuàng)建 (item_database) 數(shù)據(jù)庫(kù)筋讨。不過(guò),僅當(dāng)該數(shù)據(jù)庫(kù)不存在時(shí)才應(yīng)創(chuàng)建摸恍。否則悉罕,請(qǐng)返回現(xiàn)有數(shù)據(jù)庫(kù)。

創(chuàng)建數(shù)據(jù)庫(kù)

  1. data 軟件包中立镶,創(chuàng)建一個(gè) Kotlin 類(lèi) InventoryDatabase.kt壁袄。

  2. InventoryDatabase.kt 文件中,將 InventoryDatabase 類(lèi)設(shè)為擴(kuò)展 RoomDatabaseabstract 類(lèi)媚媒。

  3. 為該類(lèi)添加 @Database 注解嗜逻。請(qǐng)忽略缺失參數(shù)錯(cuò)誤,我們將在下一步中修復(fù)該錯(cuò)誤缭召。

import androidx.room.Database
import androidx.room.RoomDatabase

@Database
abstract class InventoryDatabase : RoomDatabase() {}

@Database 注解需要幾個(gè)參數(shù)栈顷,以便 Room 能構(gòu)建數(shù)據(jù)庫(kù)逆日。

  1. Item 指定為包含 entities 列表的唯一類(lèi)。

  2. version 設(shè)為 1萄凤。每當(dāng)您更改數(shù)據(jù)庫(kù)表的架構(gòu)時(shí)室抽,都必須提升版本號(hào)。

  3. exportSchema 設(shè)為 false靡努,這樣就不會(huì)保留架構(gòu)版本記錄的備份坪圾。

@Database(entities = [Item::class], version = 1, exportSchema = false)
  1. 在類(lèi)的主體內(nèi),聲明一個(gè)返回 ItemDao 的抽象函數(shù)惑朦,以便數(shù)據(jù)庫(kù)了解 DAO神年。
abstract fun itemDao(): ItemDao
  1. 在抽象函數(shù)下方,定義一個(gè) companion object行嗤,以允許訪問(wèn)用于創(chuàng)建或獲取數(shù)據(jù)庫(kù)的方法,并將類(lèi)名稱(chēng)用作限定符垛耳。
 companion object {}
  1. companion 對(duì)象內(nèi)栅屏,為數(shù)據(jù)庫(kù)聲明一個(gè)私有的可為 null 變量 Instance,并將其初始化為 null堂鲜。

Instance 變量將在數(shù)據(jù)庫(kù)創(chuàng)建后保留對(duì)數(shù)據(jù)庫(kù)的引用栈雳。這有助于保持在任意時(shí)間點(diǎn)都只有一個(gè)打開(kāi)的數(shù)據(jù)庫(kù)實(shí)例,因?yàn)檫@種資源的創(chuàng)建和維護(hù)成本極高缔莲。

  1. Instance 添加 @Volatile 注解哥纫。

volatile 變量的值絕不會(huì)緩存,所有讀寫(xiě)操作都將在主內(nèi)存中完成痴奏。這些功能有助于確保 Instance 的值始終是最新的蛀骇,并且對(duì)所有執(zhí)行線程都相同。也就是說(shuō)读拆,一個(gè)線程對(duì) Instance 所做的更改會(huì)立即對(duì)所有其他線程可見(jiàn)擅憔。

@Volatile
private var Instance: InventoryDatabase? = null
  1. Instance 下但仍在 companion 對(duì)象內(nèi),定義 getDatabase() 方法并提供數(shù)據(jù)庫(kù)構(gòu)建器所需的 Context 參數(shù)檐晕。

  2. 返回類(lèi)型 InventoryDatabase暑诸。

import android.content.Context

fun getDatabase(context: Context): InventoryDatabase {}

多個(gè)線程可能會(huì)同時(shí)請(qǐng)求數(shù)據(jù)庫(kù)實(shí)例,導(dǎo)致產(chǎn)生兩個(gè)數(shù)據(jù)庫(kù)辟灰,而不是一個(gè)个榕。此問(wèn)題稱(chēng)為競(jìng)態(tài)條件。封裝代碼以在 synchronized 塊內(nèi)獲取數(shù)據(jù)庫(kù)意味著一次只有一個(gè)執(zhí)行線程可以進(jìn)入此代碼塊芥喇,從而確保數(shù)據(jù)庫(kù)僅初始化一次西采。

  1. getDatabase() 內(nèi),返回 Instance 變量乃坤;如果 Instance 為 null 值苛让,請(qǐng)?jiān)?synchronized{} 塊內(nèi)對(duì)其進(jìn)行初始化沟蔑。請(qǐng)使用 elvis 運(yùn)算符 (?:) 執(zhí)行此操作。

  2. 傳入伴生對(duì)象 this狱杰。您將在后續(xù)步驟中修復(fù)該錯(cuò)誤瘦材。

return Instance ?: synchronized(this) { }
  1. 在同步的代碼塊內(nèi),使用數(shù)據(jù)庫(kù)構(gòu)建器獲取數(shù)據(jù)庫(kù)仿畸。繼續(xù)忽略錯(cuò)誤食棕,您將在后續(xù)步驟中修復(fù)這些錯(cuò)誤。
import androidx.room.Room

Room.databaseBuilder()
  1. synchronized 代碼塊內(nèi)错沽,使用數(shù)據(jù)庫(kù)構(gòu)建器獲取數(shù)據(jù)庫(kù)簿晓。將應(yīng)用上下文、數(shù)據(jù)庫(kù)類(lèi)和數(shù)據(jù)庫(kù)的名稱(chēng) item_database 傳入 Room.databaseBuilder() 中千埃。
Room.databaseBuilder(context, InventoryDatabase::class.java, "item_database")

Android Studio 會(huì)生成“類(lèi)型不匹配”錯(cuò)誤憔儿。如需消除此錯(cuò)誤,必須在后續(xù)步驟中添加 build()放可。

  1. 將所需的遷移策略添加到構(gòu)建器中谒臼。使用 . fallbackToDestructiveMigration()
.fallbackToDestructiveMigration()

注意:通常耀里,會(huì)為遷移對(duì)象提供在架構(gòu)發(fā)生更改時(shí)使用的遷移策略蜈缤。遷移對(duì)象是發(fā)揮以下作用的對(duì)象:定義如何獲取舊架構(gòu)的所有行并將其轉(zhuǎn)換為新架構(gòu)中的行,使數(shù)據(jù)不會(huì)丟失冯挎。遷移不在此 討論范圍內(nèi)底哥,但該術(shù)語(yǔ)是指當(dāng)架構(gòu)更改時(shí),我們需要在不丟失數(shù)據(jù)的情況下遷移數(shù)據(jù)房官。由于這是一個(gè)示例應(yīng)用趾徽,因此一個(gè)簡(jiǎn)單的替代方案是銷(xiāo)毀并重建數(shù)據(jù)庫(kù),這意味著商品目錄數(shù)據(jù)會(huì)丟失翰守。例如附较,如果您更改實(shí)體類(lèi)中的某些內(nèi)容(例如添加新參數(shù)),則可以允許應(yīng)用刪除并重新初始化數(shù)據(jù)庫(kù)潦俺。

  1. 如需創(chuàng)建數(shù)據(jù)庫(kù)實(shí)例拒课,請(qǐng)調(diào)用 .build()。此調(diào)用會(huì)消除 Android Studio 錯(cuò)誤事示。
.build()
  1. build() 之后早像,添加一個(gè) also 代碼塊并分配 Instance = it 以保留對(duì)最近創(chuàng)建的數(shù)據(jù)庫(kù)實(shí)例的引用。
.also { Instance = it }
  1. synchronized 代碼塊的末尾肖爵,返回 instance卢鹦。最終代碼如下所示:
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

/**
* Database class with a singleton Instance object.
*/
@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class InventoryDatabase : RoomDatabase() {

    abstract fun itemDao(): ItemDao

    companion object {
        @Volatile
        private var Instance: InventoryDatabase? = null

        fun getDatabase(context: Context): InventoryDatabase {
            // if the Instance is not null, return it, otherwise create a new database instance.
            return Instance ?: synchronized(this) {
                Room.databaseBuilder(context, InventoryDatabase::class.java, "item_database")
                    .build()
                    .also { Instance = it }
            }
        }
    }
}

提示:可以將此代碼用作未來(lái)項(xiàng)目的模板。創(chuàng)建 RoomDatabase 實(shí)例的方式與前面步驟中的過(guò)程類(lèi)似〖阶裕可能必須替換特定于實(shí)際的應(yīng)用的實(shí)體和 DAO揉稚。

  1. 構(gòu)建代碼以確保沒(méi)有錯(cuò)誤。

實(shí)現(xiàn)存儲(chǔ)庫(kù)

實(shí)現(xiàn) ItemsRepository 接口和 OfflineItemsRepository 類(lèi)熬粗,以從數(shù)據(jù)庫(kù)提供 get搀玖、insertdeleteupdate 實(shí)體驻呐。

  1. data 軟件包下創(chuàng)建 ItemsRepository.kt 文件灌诅。
  2. 將以下函數(shù)添加到映射到 DAO 實(shí)現(xiàn)的接口。
import kotlinx.coroutines.flow.Flow

/**
* Repository that provides insert, update, delete, and retrieve of [Item] from a given data source.
*/
interface ItemsRepository {
    /**
     * Retrieve all the items from the given data source.
     */
    fun getAllItemsStream(): Flow<List<Item>>

    /**
     * Retrieve an item from the given data source that matches with the [id].
     */
    fun getItemStream(id: Int): Flow<Item?>

    /**
     * Insert item in the data source
     */
    suspend fun insertItem(item: Item)

    /**
     * Delete item from the data source
     */
    suspend fun deleteItem(item: Item)

    /**
     * Update item in the data source
     */
    suspend fun updateItem(item: Item)
}
  1. data 軟件包下創(chuàng)建 OfflineItemsRepository.kt 文件含末。
  2. 傳入 ItemDao 類(lèi)型的構(gòu)造函數(shù)參數(shù)猜拾。
class OfflineItemsRepository(private val itemDao: ItemDao) : ItemsRepository
  1. OfflineItemsRepository 類(lèi)中,替換 ItemsRepository 接口中定義的函數(shù)佣盒,并從 ItemDao 調(diào)用相應(yīng)的函數(shù)挎袜。
import kotlinx.coroutines.flow.Flow

class OfflineItemsRepository(private val itemDao: ItemDao) : ItemsRepository {
    override fun getAllItemsStream(): Flow<List<Item>> = itemDao.getAllItems()

    override fun getItemStream(id: Int): Flow<Item?> = itemDao.getItem(id)

    override suspend fun insertItem(item: Item) = itemDao.insert(item)

    override suspend fun deleteItem(item: Item) = itemDao.delete(item)

    override suspend fun updateItem(item: Item) = itemDao.update(item)
}

實(shí)現(xiàn) AppContainer 類(lèi)

將實(shí)例化數(shù)據(jù)庫(kù)并將 DAO 實(shí)例傳遞給 OfflineItemsRepository 類(lèi)。

  1. data 軟件包下創(chuàng)建 AppContainer.kt 文件肥惭。
  2. ItemDao() 實(shí)例傳入 OfflineItemsRepository 構(gòu)造函數(shù)宋雏。
  3. 通過(guò)對(duì) InventoryDatabase 類(lèi)調(diào)用 getDatabase() 并傳入上下文來(lái)實(shí)例化數(shù)據(jù)庫(kù)實(shí)例,并調(diào)用 .itemDao() 以創(chuàng)建 Dao 的實(shí)例务豺。
override val itemsRepository: ItemsRepository by lazy {
    OfflineItemsRepository(InventoryDatabase.getDatabase(context).itemDao())
}

現(xiàn)在,已經(jīng)擁有了可與 Room 搭配使用的所有構(gòu)建塊嗦明。該代碼會(huì)編譯并運(yùn)行笼沥,但現(xiàn)在無(wú)法判斷它是否確實(shí)能正常運(yùn)行。因此娶牌,這正是測(cè)試數(shù)據(jù)庫(kù)的好時(shí)機(jī)奔浅。為了完成測(cè)試,需要使用 ViewModel 與數(shù)據(jù)庫(kù)通信诗良。

添加保存功能

到目前為止汹桦,已經(jīng)創(chuàng)建了一個(gè)數(shù)據(jù)庫(kù),而界面類(lèi)是起始代碼的一部分鉴裹。為了保存應(yīng)用的瞬態(tài)數(shù)據(jù)舞骆,同時(shí)也為了訪問(wèn)數(shù)據(jù)庫(kù),需要?jiǎng)?chuàng)建 ViewModel径荔。 ViewModel 通過(guò) DAO 與數(shù)據(jù)庫(kù)交互督禽,并為界面提供數(shù)據(jù)。所有數(shù)據(jù)庫(kù)操作都必須在主界面線程之外運(yùn)行总处,使用協(xié)程和 viewModelScope 可以做到這一點(diǎn)狈惫。

界面狀態(tài)類(lèi)演示

在項(xiàng)目的<packageName>基礎(chǔ)軟件包下創(chuàng)建包名和文件ui/item/ItemEntryViewModel.kt 文件。ItemUiState 數(shù)據(jù)類(lèi)表示商品的界面狀態(tài)鹦马。ItemDetails 數(shù)據(jù)類(lèi)表示單個(gè)商品胧谈。

下面代碼演示提供了三個(gè)擴(kuò)展函數(shù):

  • ItemDetails.toItem() 擴(kuò)展函數(shù)會(huì)將 ItemUiState 界面狀態(tài)對(duì)象轉(zhuǎn)換為 Item 實(shí)體類(lèi)型忆肾。
  • Item.toItemUiState() 擴(kuò)展函數(shù)會(huì)將 Item Room 實(shí)體對(duì)象轉(zhuǎn)換為 ItemUiState 界面狀態(tài)類(lèi)型。
  • Item.toItemDetails() 擴(kuò)展函數(shù)會(huì)將 Item Room 實(shí)體對(duì)象轉(zhuǎn)換為 ItemDetails菱肖。
// No need to copy, this is part of starter code
/**
* Represents Ui State for an Item.
*/
data class ItemUiState(
    val itemDetails: ItemDetails = ItemDetails(),
    val isEntryValid: Boolean = false
)

data class ItemDetails(
    val id: Int = 0,
    val name: String = "",
    val price: String = "",
    val quantity: String = "",
)

/**
* Extension function to convert [ItemDetails] to [Item]. If the value of [ItemDetails.price] is
* not a valid [Double], then the price will be set to 0.0. Similarly if the value of
* [ItemDetails.quantity] is not a valid [Int], then the quantity will be set to 0
*/
fun ItemDetails.toItem(): Item = Item(
    id = id,
    name = name,
    price = price.toDoubleOrNull() ?: 0.0,
    quantity = quantity.toIntOrNull() ?: 0
)

fun Item.formatedPrice(): String {
    return NumberFormat.getCurrencyInstance().format(price)
}

/**
* Extension function to convert [Item] to [ItemUiState]
*/
fun Item.toItemUiState(isEntryValid: Boolean = false): ItemUiState = ItemUiState(
    itemDetails = this.toItemDetails(),
    isEntryValid = isEntryValid
)

/**
* Extension function to convert [Item] to [ItemDetails]
*/
fun Item.toItemDetails(): ItemDetails = ItemDetails(
    id = id,
    name = name,
    price = price.toString(),
    quantity = quantity.toString()
)

以上代碼可以在視圖模型中使用上面的類(lèi)來(lái)讀取和更新界面客冈。

更新 ItemEntry ViewModel

將存儲(chǔ)庫(kù)傳遞給 ItemEntryViewModel.kt 文件。還需要將在 Add Item 界面中輸入的商品詳情保存到數(shù)據(jù)庫(kù)蔑滓。

  1. 請(qǐng)注意 ItemEntryViewModel 類(lèi)中的 validateInput() 私有函數(shù)郊酒。
// No need to copy over, this is part of starter code
private fun validateInput(uiState: ItemDetails = itemUiState.itemDetails): Boolean {
    return with(uiState) {
        name.isNotBlank() && price.isNotBlank() && quantity.isNotBlank()
    }
}

上面的函數(shù)會(huì)檢查 namepricequantity 是否為空键袱。在數(shù)據(jù)庫(kù)中添加或更新實(shí)體之前燎窘,將使用此函數(shù)驗(yàn)證用戶輸入。

  1. 打開(kāi) ItemEntryViewModel 類(lèi)蹄咖,然后添加類(lèi)型為 ItemsRepositoryprivate 默認(rèn)構(gòu)造函數(shù)參數(shù)褐健。
import com.example.inventory.data.ItemsRepository

class ItemEntryViewModel(private val itemsRepository: ItemsRepository) : ViewModel() {
}
  1. 創(chuàng)建 ui/AppViewModelProvider.kt 并更新商品條目視圖模型的 initializer,并將倉(cāng)庫(kù)實(shí)例作為參數(shù)傳入澜汤。
object AppViewModelProvider {
    val Factory = viewModelFactory {
        // Other Initializers
        // Initializer for ItemEntryViewModel
        initializer {
            ItemEntryViewModel(inventoryApplication().container.itemsRepository)
        }
        //...
    }
}
  1. 轉(zhuǎn)到 ItemEntryViewModel.kt 文件蚜迅,在 ItemEntryViewModel 類(lèi)的末尾添加一個(gè)名為 saveItem() 的掛起函數(shù),以將一個(gè)商品插入 Room 數(shù)據(jù)庫(kù)中俊抵。此函數(shù)以非阻塞方式將數(shù)據(jù)添加到數(shù)據(jù)庫(kù)趁尼。
suspend fun saveItem() {
}
  1. 在該函數(shù)內(nèi),檢查 itemUiState 是否有效并將其轉(zhuǎn)換為 Item 類(lèi)型价卤,以便 Room 可以理解數(shù)據(jù)牡属。
  2. 對(duì) itemsRepository 調(diào)用 insertItem() 并傳入數(shù)據(jù)。界面會(huì)調(diào)用此函數(shù)谎替,以將商品詳情添加到數(shù)據(jù)庫(kù)偷溺。
suspend fun saveItem() {
    if (validateInput()) {
        itemsRepository.insertItem(itemUiState.itemDetails.toItem())
    }
}

現(xiàn)在,向數(shù)據(jù)庫(kù)添加實(shí)體所需的函數(shù)已全部添加钱贯。下面將更新界面以使用上述函數(shù)挫掏。

ItemEntryBody() 可組合函數(shù)演示

  1. ui/item/ItemEntryScreen.kt 文件中,狀態(tài)器代碼包含的 ItemEntryBody() 可組合函數(shù)會(huì)實(shí)現(xiàn)部分功能秩命。請(qǐng)查看 ItemEntryScreen() 函數(shù)調(diào)用中的 ItemEntryBody() 可組合函數(shù)尉共。
// No need to copy over, part of the starter code
ItemEntryBody(
    itemUiState = viewModel.itemUiState,
    onItemValueChange = viewModel::updateUiState,
    onSaveClick = { },
    modifier = Modifier
        .padding(innerPadding)
        .verticalScroll(rememberScrollState())
        .fillMaxWidth()
)
  1. 請(qǐng)注意,界面狀態(tài)和 updateUiState lambda 將作為函數(shù)參數(shù)傳遞弃锐。請(qǐng)查看函數(shù)定義爸邢,了解界面狀態(tài)如何更新。
// No need to copy over, part of the starter code
@Composable
fun ItemEntryBody(
    itemUiState: ItemUiState,
    onItemValueChange: (ItemUiState) -> Unit,
    onSaveClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        // ...
    ) {
        ItemInputForm(
             itemDetails = itemUiState.itemDetails,
             onValueChange = onItemValueChange,
             modifier = Modifier.fillMaxWidth()
         )
        Button(
             onClick = onSaveClick,
             enabled = itemUiState.isEntryValid,
             shape = MaterialTheme.shapes.small,
             modifier = Modifier.fillMaxWidth()
         ) {
             Text(text = stringResource(R.string.save_action))
         }
    }
}

您在此可組合函數(shù)中顯示了 ItemInputFormSave 按鈕拿愧。在 ItemInputForm() 可組合函數(shù)中杠河,您顯示了三個(gè)文本字段。只有在文本字段中輸入文本后,系統(tǒng)才會(huì)啟用 Save 按鈕券敌。如果所有文本字段中的文本均有效(非空)唾戚,則 isEntryValid 值為 true。

手機(jī)屏幕顯示:部分商品詳情已自動(dòng)填充待诅,“Save”按鈕已停用
手機(jī)屏幕顯示:商品詳情已填充叹坦,“Save”按鈕已啟用
  1. 查看 ItemInputForm() 可組合函數(shù)實(shí)現(xiàn),并注意 onValueChange 函數(shù)參數(shù)卑雁。使用用戶在文本字段中輸入的值更新 itemDetails 值募书。啟用 Save 按鈕后,itemUiState.itemDetails 便具有需要保存的值测蹲。
// No need to copy over, part of the starter code
@Composable
fun ItemEntryBody(
    //...
) {
    Column(
        // ...
    ) {
        ItemInputForm(
             itemDetails = itemUiState.itemDetails,
             //...
         )
        //...
    }
}
// No need to copy over, part of the starter code
@Composable
fun ItemInputForm(
    itemDetails: ItemDetails,
    modifier: Modifier = Modifier,
    onValueChange: (ItemUiState) -> Unit = {},
    enabled: Boolean = true
) {
    Column(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) {
        OutlinedTextField(
            value = itemUiState.name,
            onValueChange = { onValueChange(itemDetails.copy(name = it)) },
            //...
        )
        OutlinedTextField(
            value = itemUiState.price,
            onValueChange = { onValueChange(itemDetails.copy(price = it)) },
            //...
        )
        OutlinedTextField(
            value = itemUiState.quantity,
            onValueChange = { onValueChange(itemDetails.copy(quantity = it)) },
            //...
        )
    }
}

向“Save”按鈕添加點(diǎn)擊監(jiān)聽(tīng)器

為了將一切連接到一起莹捡,請(qǐng)為 Save 按鈕添加一個(gè)點(diǎn)擊處理程序。在點(diǎn)擊處理程序中扣甲,您將啟動(dòng)一個(gè)協(xié)程并調(diào)用 saveItem() 以將數(shù)據(jù)保存在 Room 數(shù)據(jù)庫(kù)中篮赢。

  1. ItemEntryScreen.kt 中的 ItemEntryScreen 可組合函數(shù)內(nèi),使用 rememberCoroutineScope() 可組合函數(shù)創(chuàng)建一個(gè)名為 coroutineScopeval琉挖。

注意:rememberCoroutineScope() 是一個(gè)可組合函數(shù)启泣,用于返回綁定到其被調(diào)用的組合的 CoroutineScope。如果您想在可組合函數(shù)外啟動(dòng)協(xié)程示辈,并確保在該作用域退出組合后取消該協(xié)程寥茫,可以使用 rememberCoroutineScope() 可組合函數(shù)。如果您需要手動(dòng)控制協(xié)程的生命周期矾麻,例如纱耻,在發(fā)生用戶事件時(shí)取消動(dòng)畫(huà),則可以使用此函數(shù)射富。

import androidx.compose.runtime.rememberCoroutineScope

val coroutineScope = rememberCoroutineScope()
  1. 更新 ItemEntryBody``() 函數(shù)調(diào)用并在 onSaveClick lambda 內(nèi)啟動(dòng)協(xié)程。
ItemEntryBody(
   // ...
    onSaveClick = {
        coroutineScope.launch {
        }
    },
    modifier = modifier.padding(innerPadding)
)
  1. 查看 ItemEntryViewModel.kt 文件中的 saveItem() 函數(shù)實(shí)現(xiàn)以檢查 itemUiState 是否有效粥帚,將 itemUiState 轉(zhuǎn)換為 Item 類(lèi)型胰耗,然后使用 itemsRepository.insertItem() 將其插入數(shù)據(jù)庫(kù)。
// No need to copy over, you have already implemented this as part of the Room implementation

suspend fun saveItem() {
    if (validateInput()) {
        itemsRepository.insertItem(itemUiState.itemDetails.toItem())
    }
}
  1. ItemEntryScreen.kt 中的 ItemEntryScreen 可組合函數(shù)內(nèi)芒涡,從協(xié)程內(nèi)調(diào)用 viewModel.saveItem() 可將該商品保存在數(shù)據(jù)庫(kù)中柴灯。
ItemEntryBody(
    // ...
    onSaveClick = {
        coroutineScope.launch {
            viewModel.saveItem()
        }
    },
    //...
)

請(qǐng)注意,沒(méi)有在 ItemEntryViewModel.kt 文件中為 saveItem() 使用 viewModelScope.launch()费尽,但在調(diào)用存儲(chǔ)庫(kù)方法時(shí)赠群,ItemEntryBody``() 需要使用該函數(shù)。您只能從協(xié)程或其他掛起函數(shù)調(diào)用掛起函數(shù)旱幼。函數(shù) viewModel.saveItem() 就是一個(gè)掛起函數(shù)查描。

  1. 構(gòu)建并運(yùn)行您的應(yīng)用。
  2. 點(diǎn)按 + FAB。
  3. Add Item 界面中冬三,添加商品詳情并點(diǎn)按 Save匀油。請(qǐng)注意,點(diǎn)按 Save 按鈕不會(huì)關(guān)閉 Add Item 界面勾笆。
手機(jī)屏幕顯示:商品詳情已填充敌蚜,“Save”按鈕已啟用
  1. onSaveClick lambda 中,在調(diào)用 viewModel.saveItem() 后添加對(duì) navigateBack() 的調(diào)用窝爪,以返回上一個(gè)界面弛车。您的 ItemEntryBody() 函數(shù)如以下代碼所示:
ItemEntryBody(
    itemUiState = viewModel.itemUiState,
    onItemValueChange = viewModel::updateUiState,
    onSaveClick = {
        coroutineScope.launch {
            viewModel.saveItem()
            navigateBack()
        }
    },
    modifier = modifier.padding(innerPadding)
)
  1. 再次運(yùn)行應(yīng)用,然后執(zhí)行相同的步驟來(lái)輸入并保存數(shù)據(jù)蒲每。

此操作會(huì)保存數(shù)據(jù)纷跛,但您在應(yīng)用中看不到商品目錄數(shù)據(jù)。下面將使用 Database Inspector 查看已保存的數(shù)據(jù)啃勉。

顯示空白商品目錄清單的應(yīng)用屏幕

使用 Database Inspector 查看數(shù)據(jù)庫(kù)內(nèi)容

借助 Database Inspector忽舟,可以在應(yīng)用運(yùn)行時(shí)檢查、查詢和修改應(yīng)用的數(shù)據(jù)庫(kù)淮阐。此功能對(duì)于數(shù)據(jù)庫(kù)調(diào)試尤為有用叮阅。Database Inspector 可處理普通的 SQLite 數(shù)據(jù)庫(kù)以及在 SQLite 的基礎(chǔ)上構(gòu)建的庫(kù)(例如 Room)。Database Inspector 在搭載 API 級(jí)別 26 的模擬器/設(shè)備上使用效果最佳泣特。

注意:Database Inspector 只能處理 API 級(jí)別 26 及更高版本的 Android 操作系統(tǒng)中所包含的 SQLite 庫(kù)浩姥。它無(wú)法處理與您的應(yīng)用捆綁的其他 SQLite 庫(kù)。

  1. 在搭載 API 級(jí)別 26 或更高版本的模擬器或已連接設(shè)備上運(yùn)行您的應(yīng)用(如果您尚未這樣做)状您。
  2. 在 Android Studio 中勒叠,從菜單欄中依次選擇 View > Tool Windows > App Inspection
  3. 選擇 Database Inspector 標(biāo)簽頁(yè)膏孟。
  4. Database Inspector 窗格中眯分,從下拉菜單中選擇 com.example.inventory(如果尚未選擇)。Inventory 應(yīng)用中的 item_database 將顯示于 Databases 窗格中柒桑。
  1. Databases 窗格中展開(kāi) item_database 的節(jié)點(diǎn)弊决,然后選擇要檢查的 Item。如果 Databases 窗格為空魁淳,請(qǐng)使用模擬器通過(guò) Add Item 界面向數(shù)據(jù)庫(kù)中添加一些商品飘诗。
  2. 選中 Database Inspector 中的 Live updates 復(fù)選框,以便隨著與模擬器或設(shè)備中正在運(yùn)行的應(yīng)用互動(dòng)而自動(dòng)更新呈現(xiàn)的數(shù)據(jù)界逛。

獲取解決方案代碼

此 Codelab 的解決方案代碼位于 GitHub 倉(cāng)庫(kù)中昆稿。如需下載完成后的 Codelab 代碼,請(qǐng)使用以下 Git 命令:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-inventory-app.git
$ cd basic-android-kotlin-compose-training-inventory-app
$ git checkout room

或者息拜,也可以下載 ZIP 文件形式的代碼庫(kù)溉潭,將其解壓縮并在 Android Studio 中打開(kāi)净响。

file_download下載 ZIP 文件

?注意:解決方案代碼位于所下載代碼庫(kù)的 room 分支中。

總結(jié)

  • 將表定義為帶有 @Entity 注解的數(shù)據(jù)類(lèi)岛抄。將帶有 @ColumnInfo 注解的屬性定義為表中的列别惦。
  • 將數(shù)據(jù)訪問(wèn)對(duì)象 (DAO) 定義為帶有 @Dao 注解的接口。DAO 用于將 Kotlin 函數(shù)映射到數(shù)據(jù)庫(kù)查詢夫椭。
  • 使用注解來(lái)定義 @Insert掸掸、@Delete@Update 函數(shù)。
  • @Query 注解和作為參數(shù)的 SQLite 查詢字符串用于所有其他查詢蹭秋。
  • 使用 Database Inspector 查看 Android SQLite 數(shù)據(jù)庫(kù)中保存的數(shù)據(jù)扰付。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市仁讨,隨后出現(xiàn)的幾起案子羽莺,更是在濱河造成了極大的恐慌,老刑警劉巖洞豁,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盐固,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡丈挟,警方通過(guò)查閱死者的電腦和手機(jī)刁卜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)曙咽,“玉大人蛔趴,你說(shuō)我怎么就攤上這事±欤” “怎么了孝情?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)洒嗤。 經(jīng)常有香客問(wèn)我箫荡,道長(zhǎng),這世上最難降的妖魔是什么渔隶? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任羔挡,我火速辦了婚禮,結(jié)果婚禮上派撕,老公的妹妹穿的比我還像新娘婉弹。我一直安慰自己睬魂,他們只是感情好终吼,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著氯哮,像睡著了一般际跪。 火紅的嫁衣襯著肌膚如雪商佛。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,741評(píng)論 1 289
  • 那天姆打,我揣著相機(jī)與錄音良姆,去河邊找鬼。 笑死幔戏,一個(gè)胖子當(dāng)著我的面吹牛玛追,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播闲延,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼痊剖,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了垒玲?” 一聲冷哼從身側(cè)響起陆馁,我...
    開(kāi)封第一講書(shū)人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎合愈,沒(méi)想到半個(gè)月后叮贩,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡佛析,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年益老,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片说莫。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡杨箭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出储狭,到底是詐尸還是另有隱情互婿,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布辽狈,位于F島的核電站慈参,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏刮萌。R本人自食惡果不足惜驮配,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望着茸。 院中可真熱鬧壮锻,春花似錦、人聲如沸涮阔。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)敬特。三九已至掰邢,卻和暖如春牺陶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背辣之。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工掰伸, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人怀估。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓狮鸭,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親多搀。 傳聞我的和親對(duì)象是個(gè)殘疾皇子怕篷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容