大多數(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ù)源性誉。
前提條件
能夠使用 Jetpack Compose 為 Android 應(yīng)用構(gòu)建基本界面窿吩。
能夠使用
Text
、Icon
错览、IconButton
和LazyColumn
等可組合函數(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)下載起始代碼:
或者初坠,也可以克隆該代碼的 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ù)中瀏覽該代碼。
起始代碼概覽
在 Android Studio 中打開(kāi)包含起始代碼的項(xiàng)目碟刺。
在 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)浩村。
該應(yīng)用未顯示任何商品目錄數(shù)據(jù)。
點(diǎn)按懸浮操作按鈕 (FAB) 向數(shù)據(jù)庫(kù)中添加新商品占哟。
應(yīng)用會(huì)轉(zhuǎn)到一個(gè)新界面心墅,可以在其中輸入新商品的詳情。
顯示空白商品目錄的手機(jī)屏幕
|
手機(jī)屏幕中顯示“Add item”界面
|
---|---|
起始代碼存在的問(wèn)題
在 Add Item 界面中榨乎,輸入商品的詳情怎燥,例如名稱(chēng)、價(jià)格和數(shù)量蜜暑。
點(diǎn)按 Save铐姚。Add Item 界面未關(guān)閉,但您可以使用返回鍵返回史煎。保存功能未實(shí)現(xiàn)谦屑,因此系統(tǒng)不會(huì)保存商品詳情驳糯。
請(qǐng)注意,該應(yīng)用尚未完成氢橙,Save 按鈕功能尚未實(shí)現(xiàn)酝枢。
在此 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)] 筛璧,可用于向列表中添加新商品逸绎。
ui/item/ItemEntryScreen.kt
此界面類(lèi)似于 ItemEditScreen.kt
。它們都提供了用于輸入商品詳情的文本字段夭谤。點(diǎn)按主屏幕中的 FAB 即會(huì)顯示此界面棺牧。ItemEntryViewModel.kt
是此界面的對(duì)應(yīng) ViewModel
。
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 依賴項(xiàng)
向 Gradle 文件添加所需的 Room 組件庫(kù)蛹找。
打開(kāi)模塊級(jí) Gradle 文件
build.gradle.kts (Module: InventoryApp.app)
姨伤。在
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ù)量嫂拴。
@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ù)量儡循。
data
舶吗。在
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ù)都必須是
val
或var
。數(shù)據(jù)類(lèi)不能為
abstract
作儿、open
或sealed
洛二。
? 警告:編譯器只會(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)文檔。
- 為
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
)
- 在
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)行研究烈钞。
- 為
id
屬性添加@PrimaryKey
注解泊碑,使id
成為主鍵。主鍵是一個(gè) ID毯欣,用于唯一標(biāo)識(shí)Item
表格中的每個(gè)記錄/條目
import androidx.room.PrimaryKey
@Entity(tableName = "items")
data class Item(
@PrimaryKey
val id: Int,
...
)
為
id
分配默認(rèn)值0
蛾狗,這樣才能使id
自動(dòng)生成id
值。將參數(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:
- 在
data
軟件包中诗茎,創(chuàng)建 Kotlin 接口ItemDao.kt
工坊。
- 為接口
ItemDao
添加@Dao
注解。
import androidx.room.Dao
@Dao
interface ItemDao {
}
在該接口的主體內(nèi)添加
@Insert
注解。在
@Insert
下王污,添加一個(gè)insert()
函數(shù)罢吃,該函數(shù)將Entity
類(lèi)的實(shí)例item
作為其參數(shù)。使用
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窟却。
- 添加參數(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ù)中悴品。
- 添加一個(gè)帶有
@Update
注解的新函數(shù),該函數(shù)接受Item
作為參數(shù)简烘。
更新的實(shí)體與傳入的實(shí)體具有相同的主鍵苔严。您可以更新該實(shí)體的部分或全部其他屬性。
- 與
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 查詢。
- 編寫(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
添加
@Query
注解向拆。使用上一步中的 SQLite 查詢作為
@Query
注解的字符串參數(shù)。向
@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")
- 在
@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)用手负。
添加
@Query
注解和getAllItems()
函數(shù)涤垫。讓 SQLite 查詢返回
item
表中的所有列,依升序排序竟终。讓
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>>
}
- 盡管您不會(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ò)展
RoomDatabase
的public 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è)為單例闺鲸。使用
Room
的Room.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ù)
在
data
軟件包中立镶,創(chuàng)建一個(gè) Kotlin 類(lèi)InventoryDatabase.kt
壁袄。在
InventoryDatabase.kt
文件中,將InventoryDatabase
類(lèi)設(shè)為擴(kuò)展RoomDatabase
的abstract
類(lèi)媚媒。為該類(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ù)逆日。
將
Item
指定為包含entities
列表的唯一類(lèi)。將
version
設(shè)為1
萄凤。每當(dāng)您更改數(shù)據(jù)庫(kù)表的架構(gòu)時(shí)室抽,都必須提升版本號(hào)。將
exportSchema
設(shè)為false
靡努,這樣就不會(huì)保留架構(gòu)版本記錄的備份坪圾。
@Database(entities = [Item::class], version = 1, exportSchema = false)
- 在類(lèi)的主體內(nèi),聲明一個(gè)返回
ItemDao
的抽象函數(shù)惑朦,以便數(shù)據(jù)庫(kù)了解 DAO神年。
abstract fun itemDao(): ItemDao
- 在抽象函數(shù)下方,定義一個(gè)
companion object
行嗤,以允許訪問(wèn)用于創(chuàng)建或獲取數(shù)據(jù)庫(kù)的方法,并將類(lèi)名稱(chēng)用作限定符垛耳。
companion object {}
- 在
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ù)成本極高缔莲。
- 為
Instance
添加@Volatile
注解哥纫。
volatile 變量的值絕不會(huì)緩存,所有讀寫(xiě)操作都將在主內(nèi)存中完成痴奏。這些功能有助于確保 Instance
的值始終是最新的蛀骇,并且對(duì)所有執(zhí)行線程都相同。也就是說(shuō)读拆,一個(gè)線程對(duì) Instance
所做的更改會(huì)立即對(duì)所有其他線程可見(jiàn)擅憔。
@Volatile
private var Instance: InventoryDatabase? = null
在
Instance
下但仍在companion
對(duì)象內(nèi),定義getDatabase()
方法并提供數(shù)據(jù)庫(kù)構(gòu)建器所需的Context
參數(shù)檐晕。返回類(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ù)僅初始化一次西采。
在
getDatabase()
內(nèi),返回Instance
變量乃坤;如果Instance
為 null 值苛让,請(qǐng)?jiān)?synchronized{}
塊內(nèi)對(duì)其進(jìn)行初始化沟蔑。請(qǐng)使用 elvis 運(yùn)算符 (?:
) 執(zhí)行此操作。傳入伴生對(duì)象
this
狱杰。您將在后續(xù)步驟中修復(fù)該錯(cuò)誤瘦材。
return Instance ?: synchronized(this) { }
- 在同步的代碼塊內(nèi),使用數(shù)據(jù)庫(kù)構(gòu)建器獲取數(shù)據(jù)庫(kù)仿畸。繼續(xù)忽略錯(cuò)誤食棕,您將在后續(xù)步驟中修復(fù)這些錯(cuò)誤。
import androidx.room.Room
Room.databaseBuilder()
- 在
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()
放可。
- 將所需的遷移策略添加到構(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ù)潦俺。
- 如需創(chuàng)建數(shù)據(jù)庫(kù)實(shí)例拒课,請(qǐng)調(diào)用
.build()
。此調(diào)用會(huì)消除 Android Studio 錯(cuò)誤事示。
.build()
- 在
build()
之后早像,添加一個(gè)also
代碼塊并分配Instance = it
以保留對(duì)最近創(chuàng)建的數(shù)據(jù)庫(kù)實(shí)例的引用。
.also { Instance = it }
- 在
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揉稚。
- 構(gòu)建代碼以確保沒(méi)有錯(cuò)誤。
實(shí)現(xiàn)存儲(chǔ)庫(kù)
實(shí)現(xiàn) ItemsRepository
接口和 OfflineItemsRepository
類(lèi)熬粗,以從數(shù)據(jù)庫(kù)提供 get
搀玖、insert
、delete
和 update
實(shí)體驻呐。
- 在
data
軟件包下創(chuàng)建ItemsRepository.kt
文件灌诅。 - 將以下函數(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)
}
- 在
data
軟件包下創(chuàng)建OfflineItemsRepository.kt
文件含末。 - 傳入
ItemDao
類(lèi)型的構(gòu)造函數(shù)參數(shù)猜拾。
class OfflineItemsRepository(private val itemDao: ItemDao) : ItemsRepository
- 在
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)。
- 在
data
軟件包下創(chuàng)建AppContainer.kt
文件肥惭。 - 將
ItemDao()
實(shí)例傳入OfflineItemsRepository
構(gòu)造函數(shù)宋雏。 - 通過(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ù)蔑滓。
- 請(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ì)檢查 name
、price
和 quantity
是否為空键袱。在數(shù)據(jù)庫(kù)中添加或更新實(shí)體之前燎窘,將使用此函數(shù)驗(yàn)證用戶輸入。
- 打開(kāi)
ItemEntryViewModel
類(lèi)蹄咖,然后添加類(lèi)型為ItemsRepository
的private
默認(rèn)構(gòu)造函數(shù)參數(shù)褐健。
import com.example.inventory.data.ItemsRepository
class ItemEntryViewModel(private val itemsRepository: ItemsRepository) : ViewModel() {
}
- 創(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)
}
//...
}
}
- 轉(zhuǎn)到
ItemEntryViewModel.kt
文件蚜迅,在ItemEntryViewModel
類(lèi)的末尾添加一個(gè)名為saveItem()
的掛起函數(shù),以將一個(gè)商品插入 Room 數(shù)據(jù)庫(kù)中俊抵。此函數(shù)以非阻塞方式將數(shù)據(jù)添加到數(shù)據(jù)庫(kù)趁尼。
suspend fun saveItem() {
}
- 在該函數(shù)內(nèi),檢查
itemUiState
是否有效并將其轉(zhuǎn)換為Item
類(lèi)型价卤,以便 Room 可以理解數(shù)據(jù)牡属。 - 對(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ù)演示
- 在
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()
)
- 請(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ù)中顯示了 ItemInputForm
和 Save 按鈕拿愧。在 ItemInputForm()
可組合函數(shù)中杠河,您顯示了三個(gè)文本字段。只有在文本字段中輸入文本后,系統(tǒng)才會(huì)啟用 Save 按鈕券敌。如果所有文本字段中的文本均有效(非空)唾戚,則 isEntryValid
值為 true。
手機(jī)屏幕顯示:部分商品詳情已自動(dòng)填充待诅,“Save”按鈕已停用
|
手機(jī)屏幕顯示:商品詳情已填充叹坦,“Save”按鈕已啟用
|
---|---|
- 查看
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ù)中篮赢。
- 在
ItemEntryScreen.kt
中的ItemEntryScreen
可組合函數(shù)內(nèi),使用rememberCoroutineScope()
可組合函數(shù)創(chuàng)建一個(gè)名為coroutineScope
的val
琉挖。
注意: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()
- 更新
ItemEntryBody``()
函數(shù)調(diào)用并在onSaveClick
lambda 內(nèi)啟動(dòng)協(xié)程。
ItemEntryBody(
// ...
onSaveClick = {
coroutineScope.launch {
}
},
modifier = modifier.padding(innerPadding)
)
- 查看
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())
}
}
- 在
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ù)查描。
- 構(gòu)建并運(yùn)行您的應(yīng)用。
- 點(diǎn)按 + FAB。
- 在 Add Item 界面中冬三,添加商品詳情并點(diǎn)按 Save匀油。請(qǐng)注意,點(diǎn)按 Save 按鈕不會(huì)關(guān)閉 Add Item 界面勾笆。
- 在
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)
)
- 再次運(yùn)行應(yīng)用,然后執(zhí)行相同的步驟來(lái)輸入并保存數(shù)據(jù)蒲每。
此操作會(huì)保存數(shù)據(jù)纷跛,但您在應(yīng)用中看不到商品目錄數(shù)據(jù)。下面將使用 Database Inspector 查看已保存的數(shù)據(jù)啃勉。
使用 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ù)。
- 在搭載 API 級(jí)別 26 或更高版本的模擬器或已連接設(shè)備上運(yùn)行您的應(yīng)用(如果您尚未這樣做)状您。
- 在 Android Studio 中勒叠,從菜單欄中依次選擇 View > Tool Windows > App Inspection。
- 選擇 Database Inspector 標(biāo)簽頁(yè)膏孟。
- 在 Database Inspector 窗格中眯分,從下拉菜單中選擇
com.example.inventory
(如果尚未選擇)。Inventory 應(yīng)用中的 item_database 將顯示于 Databases 窗格中柒桑。
- 在 Databases 窗格中展開(kāi) item_database 的節(jié)點(diǎn)弊决,然后選擇要檢查的 Item。如果 Databases 窗格為空魁淳,請(qǐng)使用模擬器通過(guò) Add Item 界面向數(shù)據(jù)庫(kù)中添加一些商品飘诗。
- 選中 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)净响。
?注意:解決方案代碼位于所下載代碼庫(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ù)扰付。