建構一個 inventory app,使用 Room 將庫存商品儲存至 SQLite database。
- 如何使用 Room library 建立 SQLite database 並與之互動。
- 如何建立 entity、DAO 和 database classes。
- 如何使用 data access object (DAO) 將 Kotlin function 對應至 SQL 查詢(queries)。
Persist data
大多數的 production quality apps 都有需要儲存的 data,即便使用者關閉 app 也不例外。舉例來說,app 可能會儲存歌曲播放清單、待辦事項清單、支出和收入記錄、星座目錄或個人資料記錄。在大部分情況下,您可以使用 database 來儲存這些持續性資料(persistent data)。
Room 是 Android Jetpack 中的 persistence library。Room 是 SQLite database 頂端的抽象層(abstraction layer)。SQLite 使用 specialized language (SQL) 執行 database 作業。Room 不直接使用 SQLite,因此簡化了設定 database 並與之互動的過程。Room 也提供 SQLite statements 的編譯時間(compile-time)檢查。
下圖說明了 Room 如何配合本課程推薦的整體架構。

App overview
在本程式碼研究室中,您將使用名為 Inventory app 的 starter app,並透過 Room library 將 database layer 加入其中。最終版本的 app 會使用 RecyclerView
顯示 inventory database 的items list。使用者可以選擇在 inventory database 中新增(new) item、更新(update)其中的現有 item,以及刪除(delete)其中的 item (您將於下一個程式碼研究室完成 app 功能)。
以下是最終版本 app 的螢幕截圖。

注意:以上螢幕截圖均來自課程最後的最終版本 app,而非本程式碼研究室最後的 app。此處提供這些螢幕截圖,以便您大概瞭解最終版本的 app。
下載範例程式碼
如果您使用 GitHub 中的範例程式碼,請注意資料夾名稱是 android-basics-kotlin-inventory-app-starter
。在 Android Studio 中開啟專案時,請選取這個資料夾。
範例程式碼網址:
https://github.com/google-developer-training/android-basics-kotlin-inventory-app/tree/starter
分支名稱:starter
範例程式碼相關問題
- 在「Add Item」畫面中,輸入 item 的詳細資料(details)。輕觸「Save」(儲存)。目前,此 add item fragment 未關閉。使用系統 back 鍵返回。系統不會儲存 new item,也不會將其列在 inventory 畫面中。請注意,此 app 並不完整,且未實作「Save」button 功能。

在本程式碼研究室中,您將新增 app 的 database 部分,該部分會將 inventory 詳細資料(details)儲存至 SQLite database。您將使用 Room persistence library 與 SQLite database 互動。
程式碼逐步操作說明
您下載的範例程式碼包含已為您預先設計的螢幕版面配置。在本課程中,您將重點瞭解實作 database 邏輯。以下是一些檔案的簡要逐步操作說明,協助您快速上手。
main_activity.xml
App 中代管(hosts)所有其他 fragment 的 main activity。onCreate()
method 會從NavHostFragment
擷取NavController
,並設定與NavController
搭配使用的 action bar。item_list_fragment.xml
App 中顯示的第一個畫面。主要包含RecyclerView
和懸浮動作按鈕 (FAB)。您會在稍後的課程中實作RecyclerView
。fragment_add_item.xml
這個 layout 包含 text fields,用於輸入要被 added 的 new inventory item 的詳細資料(details)。ItemListFragment.kt
這個 fragment 主要包含樣板程式碼。在onViewCreated()
method 中,在 FAB 上設定 click listener 以導覽(navigate)至AddItemFragment
AddItemFragment.kt
這個 fragment 用於向 database 加入 new items。onCreateView()
function 會初始化binding variable,onDestroyView()
function 則會在 destroying fragment 前隱藏鍵盤(keyboard)。
★Room 的主要 components
Kotlin 可透過引入 data classes,輕鬆處理 data。這些 data 可存取,並可透過 function 呼叫加以修改。但在 database 中,您需要使用 tables 和 queries 來存取及修改 data。以下 Room components 能讓這些工作流程順暢運作。
Room 有三個主要 components:
- Data entities,代表 app database 中的 tables。它們可用於更新 tables 中以 rows 形式儲存的 data,也可用於建立要插入(insertion)的 new rows。
- Data access object (DAO) 為 app 提供了各種 methods,用來擷取(retrieve)、更新(update)、插入(insert)及刪除(delete) database 中的 data。
- Database class可存放 database,是 app database 連線的主要存取點(main access point)。Database class 為您的 app 提供了與該 database 關聯的 DAO instances。
您將在稍後的程式碼研究室中實作這些 components,並進一步瞭解它們。下圖演示了 Room 的各 components 如何一起工作以與 database 互動。

新增 Room libraries
在這項工作中,您要將必要的 Room component libraries 加入 Gradle 檔案。
開啟 module level 的 Gradle 檔案 build.gradle (Module: InventoryApp.app)
。在 dependencies
區塊中,為 Room library 新增下列 dependencies。
1 | // Room |
room_version
可至 AndroidX 查詢最新版本。
重要:因使用了 kapt,需在 app-level build.gradle
檔案的 plugins 中新增 id 'kotlin-kapt'
。
1 | plugins { |
建立 item Entity
Entity class 定義了 table,這個 class 的所有 instance 都代表了 database table 中的一個 row。Entity class 擁有對應項目(mappings),可用來向 Room 顯示它打算如何呈現 database 中的資訊並與之互動。在 app 中,這個 entity 會存放有關 inventory items 的資訊,例如 item name、item price 和 stock 狀況。

@Entity
註解會將 class 標示為 database Entity class。系統會為每個 entity class 建立 database table,用於存放 items。在 entity 中,每個欄位(field)都會表示為 database 中的一列 column,除非另有說明。儲存在 database 中的所有 entity instance 都必須有主鍵(primary key)。Primary key 用於唯一識別 database table 中的每個 record/entry。Primary key 一經指派即無法修改,只要存在於 database 中,指的就是 entity object。
在這項工作中,您將建立 entity class。定義 field 以儲存每個 item 的下列 inventory information。
- 用於儲存 primary key 的
Int
。 - 用於儲存 item name 的
String
。 - 用於儲存 item price 的
Double
。 - 用於儲存 stock 數量的
Int
。
在 Android Studio 中開啟範例程式碼。
在
com.example.inventory
基本 package 下方建立名為data
的 package。在
data
package 中,建立名為Item
的 Kotlin class。這個 class 將代表 app 中的 database entity。在下一個步驟中,您將新增對應欄位(fields)來儲存 inventory information。使用下列程式碼更新
Item
class 定義。宣告Int
type 的id
、String
type 的itemName
、Double
type 的itemPrice
,以及Int
type 的quantityInStock
做為 primary constructor 的參數。將id
的預設值設為0
。id
將成為 primary key,用來辨識Item
table 中每個 record/entry 的 uniquely ID。
1 | class Item( |
Refresher on primary constructor:Primary constructor 是 Kotlin class 的 class header 的一部分;出現在 class name (和 optional type 參數) 之後。
Data classes
Data classes 主要用於保存 Kotlin 中的 data。這些 class 有標示 data
keyword。Kotlin data class objects 有許多其他優勢,compiler 會自動產生公用程式(utilities),用於comparing、printing 及 copying: toString()
、copy()
、equals()
等。
範例:
1 | // Example data class with 2 properties. |
為了讓產生的程式碼保持一致且行為有意義,data classes 必須符合下列規定:
- Primary constructor 必須有至少一個參數。
- 所有 primary constructor 參數都必須標示為
val
或var
。 - Data classes 不得為
abstract
、open
、sealed
或inner
。
警告:Compiler 只會使用 primary constructor 內部定義的屬性,來自動產生 functions。產生的實作會排除在 class body 中宣告的屬性。
- 如要進一步瞭解 data classes,請參閱說明文件。
- 在
Item
class 的 class 定義前加上data
keyword,將其轉換為 data class。
1 | data class Item( |
- 在
Item
class 宣告上方,為 data class 加上註解@Entity
。使用tableName
引數提供item
做為 SQLite table name。
1 |
|
重要:當 Android Studio 顯示提示(prompted)時,請從 androidx library import Entity 和所有其他 Room 註解。例如,androidx.room.Entity。
注意:@Entity 註解有幾個可能的引數。根據預設 (@Entity 沒有引數),table name 將與 class 相同。tableName 引數可讓您提供其他更為實用的 table name。雖然 tableName 引數是可選的(optional),但我們強烈建議使用。為求簡單,請提供與 class name 相同的名稱,也就是 item。您還可以在說明文件中調查 @Entity 的其他幾個引數。
- 要將
id
作為 primary key,請為id
屬性加上註解@PrimaryKey
。將autoGenerate
參數設為true
,以便 Room 為每個 entity 產生 ID。這能保證每個 item 的 ID 都不重複。
1 |
|
- 為其餘屬性加上註解
@ColumnInfo
。ColumnInfo
註解可用來自訂與特定欄位(field)相關的 column。舉例來說,使用name
引數時,您可以為欄位(field)指定不同的 column name。
Item.kt
完整程式碼:
1 | // data class entity |
建立 item DAO
Data Access Object (DAO)
Data Access Object (DAO) 是一種模式(pattern),可透過提供 abstract interface,從 app 的其餘部分中區隔出 persistence layer。這種隔離機制符合單一責任原則(single responsibility principle)。
DAO 的功能是向 app 的其餘部分隱藏在基礎 persistence layer 中執行 database 作業所涉及的所有復雜性。這樣一來,就可以變更 data access layer,而不受使用該 data 的程式碼影響。

在這項工作中,您要為 Room 定義 Data Access Object (DAO)。Data Access Object 是 Room 的主要 components,負責定義存取 database 的 interface。
DAO 是自訂 interface,該 interface 可提供便利的 methods,用於 querying/retrieving、inserting、deleting 及 updating database。Room 會在 compile time 產生這個 class 的實作。
針對常見的 database 作業,Room library 可提供便利的註解,例如 @Insert
、@Delete
和 @Update
。除此之外,您還可以使用 @Query
註解。您可以編寫受 SQLite 支援的任何查詢(query)。
另一個好處是,當您在 Android Studio 中編寫查詢(queries)時,Compiler 會檢查 SQL 查詢是否有語法錯誤。
對於 inventory app,您需要能夠執行以下操作:
- 插入(Insert)或新增(add) ,new item。
- 更新(Update) 現有 item 的 name、price 和 quantity。
- 根據 primary key
id
,取得(Get)特定 item。 - 取得(Get)所有 items,以便顯示它們。
- 刪除(Delete) database 中的 entry。

接著在 app 中實作 item DAO:
- 在
data
package 中建立 Kotlin classItemDao.kt
。 - 將 class 定義變更為 interface,並加上註解
@Dao
。
1 |
|
- 在 interface body 中,新增
@Insert
註解。在@Insert
下方,新增insert()
function,以將Entity
classitem
的 instance 做為引數。Database 作業執行時間可能較長,因此應該會在另一個 thread 中執行。請將 function 設為 suspend function,以便從 coroutine 中呼叫。
1 |
|
- 新增引數
OnConflict
,並為其指派OnConflictStrategy.IGNORE
的值。OnConflict
引數會指示 Room 在發生衝突時應如何處理。如果 new item 的 primary key 已存在於 database 中,則OnConflictStrategy.IGNORE
策略會忽略 new item。如要進一步瞭解可用的衝突策略,請參閱說明文件。
1 | // 發生衝突時會忽略 new item |
- Room 會產生將
item
insert database 所需的所有程式碼。當從 Kotlin 程式碼呼叫insert()
時,Room 會執行 SQL 查詢(query),將 entity insert 到 database 中。
- 為一個
item
新增帶有update()
function 的@Update
註解。更新的 entity 與傳入的 entity key 相同。您可以更新 entity 的部分或全部其他屬性。類似於insert()
method,使以下update()
methodsuspend
。
1 |
|
- 新增具有
delete()
function 的@Delete
註解以刪除 item。使其成為 suspend method。@Delete
註解會刪除一個 item 或一個 item list。(您需要傳遞要刪除的 entity(s);若沒有 entity,則要在呼叫delete()
function 之前擷取 entity。)
1 |
|
剩餘的 function 沒有方便的註解,因此您必須使用 @Query
註解並提供 SQLite 查詢(queries)。
- 編寫 SQLite 查詢(query),根據指定的
id
從 item table 中擷取特定 item。接著,您要新增 Room 註解,並在後續步驟中使用修改後的下列 query。在後續步驟中,您還要透過 Room 將這項內容變更為 DAO method。 - 從
item
中選取所有 columns。 WHERE
id
符合特定值。
範例:
1 | SELECT * from item WHERE id = 1 |
- 變更上述 SQL 查詢(query),使其與 Room 註解和引數搭配使用。新增
@Query
註解,將 query 以 string 參數的形式提供給@Query
註解。將String
參數新增至@Query
,這是一個 SQLite 查詢(query),用於從 item table 中擷取 item。 - 從
item
中選取所有 columns。 WHERE
id
=:id
引數。
1 |
筆記:請留意 :id
。您可以在 Query 中使用冒號( : )來引用 function 中的引數(argument)。
1 |
|
- 在
@Query
註解下方,新增getItem()
function,這個 function 會採用Int
引數並 returnFlow<Item>
。
1 |
|
- import
kotlinx.coroutines.flow.Flow
。
使用 Flow
或 LiveData
做為 return type,可確保當 database 中的 data 有變更時,您會收到通知。建議您在 persistence layer 中使用 Flow
。Room 將隨時為您更新這個 Flow
,因此您只需要明確取得一次 data 即可。這有助於更新 inventory list,您將於下一個程式碼研究室中實作這部分內容。根據 Flow
return type,Room 也會在 background thread 上執行查詢(query)。您不需要將它明確設為 suspend
function,並在 coroutine scope 內呼叫。
- 新增具有
getItems()
function 的@Query
: - 讓 SQLite query return
item
table 中的所有 columns,以 name 遞增順序排序。 - 使
getItems()
returnItem
entities list (做為Flow
)。Room 將隨時為您更新這個Flow
,因此您只需要明確取得一次 data 即可。
1 |
|
ItemDao.kt
完整程式碼:
1 | // data access object (DAO) |
- 雖然您看不到任何明顯的變更,但請執行 app,確定沒有任何錯誤。
建立 database class
在這項工作中,您要建立 RoomDatabase,並使用您在先前工作中建立的 Entity
和 DAO。Database class 定義了 entities list 和 data access objects(DAO) list。同時也是基礎連線(connection)的主要存取點(main access point)。
Database
class 為您的 app 提供了定義的 DAO instance。反過來,app 可以使用 DAO 來擷取 database 中的 data,做為關聯(associated) data entity objects 的 instance。App 也可以使用定義的 data entity,更新對應 table 中的 rows,或是建立 new rows 來插入 data。
您需要建立 abstract RoomDatabase
class,並加上 @Database
註解。此 class 包含一個 method,可在 RoomDatabase
的 instance 不存在時建立該 instance,或者 return RoomDatabase
的現有 instance。
取得 RoomDatabase
instance 的一般程序如下:
- 建立繼承
RoomDatabase
的public abstract
class。您定義的新 abstract class 會成為 database holder。您定義的 class 為 abstract,因為Room
會為您建立實作。 - 使用
@Database
為 class 加上註解。在引數中,列出 database 的 entity 並設定 version number。 - 定義 return
ItemDao
instance 的 abstract method 或屬性(property),Room
會為您產生實作。 - 整個 app 只需要一個
RoomDatabase
instance,因此請將RoomDatabase
設為單例模式(singleton)。 - 僅在您的 (
item_database
) database 不存在的情況下,使用 Room 的Room.databaseBuilder
建立 database。否則,請 return 現有 database。
筆記:單例模式(singleton)可確保只建立一個 instance。Companion object 只會存在一個 instance,所以也屬於單例模式(singleton)。
建立 Database
- 在
data
package 中,建立 Kotlin classItemRoomDatabase.kt
。 - 在
ItemRoomDatabase.kt
檔案中,將ItemRoomDatabase
class 設為繼承RoomDatabase
的abstract
class。使用@Database
為 class 加上註解。您將在下一步中修正缺少參數的錯誤。
1 |
|
@Database
註解需要多個引數,以便Room
能夠 build database。
- 將
Item
指定為包含entities
list 的唯一 class。 - 將
version
設為1
。每次變更 database table 的結構定義(schema)時,都必須增加 version number。 - 只要將
exportSchema
設為false
,即可不保留結構定義(schema) version 紀錄的備份。
1 |
- Database 需要知道該 DAO。在 class body 中,宣告一個 return
ItemDao
的 abstract function。您可以擁有多個 DAO。
1 | abstract fun itemDao(): ItemDao |
- 在 abstract function 下方,定義
companion object
。Companion object 可讓您使用 class name 做為限定詞(qualifier),建立或取得 database。
1 | companion object { |
- 在
companion object
中,宣告 database 的 private nullable 變數INSTANCE
,並將其初始化為null
。INSTANCE
變數會在建立 database 時保留對該 database 的引用。這有助於維護在指定時間開啟的 database 單一 instance,該 instance 是建立及維護成本很高的資源。
使用 @Volatile
為 INSTANCE
加上註解。系統一律不會快取易失變數的值,所有讀取與寫入作業都會在主記憶體(main memory)中完成。這有助於確保所有執行 thread 的 INSTANCE
值保持在最新狀態且相同。這表示一個 thread 對 INSTANCE
所做的變更將立即對所有其他 thread 可見。
1 |
|
- 在
INSTANCE
下方的companion object
內,使用 database builder 所需的Context
參數定義getDatabase()
method。Return typeItemRoomDatabase
。由於getDatabase()
尚未 return 任何內容,因此系統會顯示錯誤訊息。
1 | fun getDatabase(context: Context): ItemRoomDatabase { |
- 多個 thread 可能會產生競爭狀況(race condition),並同時要求一個 database instance,如此一來就會產生兩個 database。納入程式碼以將 database 納入
synchronized
區塊中,這表示一次只能執行一個 thread,只有這個 thread 可以進入此程式碼區塊,從而確保系統只會將 database 初始化一次。
在 getDatabase()
中,return INSTANCE
變數;如果 INSTANCE
為 null,則在 synchronized{}
區塊內對其進行初始化。使用 elvis
運算子 (?:
) 執行此作業。傳入 companion object this
,也就是要在 function 區塊中鎖定的 companion object。您將在後續步驟中修正此錯誤。
1 | return INSTANCE ?: synchronized(this) { } |
筆記: A ?: B 意思是「當 A 不為 null 時就返回 A,當 A 為 null 時就返回 B」。
- 在
synchronized
區塊中,建立val
instance 變數,並使用 database builder 取得 database。您還有錯誤有待在後續步驟中修正。
1 | val instance = Room.databaseBuilder() |
- 在
synchronized
區塊的結尾,returninstance
。
1 | return instance |
- 在
synchronized
區塊中,初始化instance
變數,並使用 database builder 取得 database。將 application context、database class 以及 database nameitem_database
傳遞給Room.databaseBuilder()
。
1 | val instance = Room.databaseBuilder( |
- Android Studio 會產生「 type mismatch」錯誤。如要移除這項錯誤,請按照下列步驟新增遷移策略和
build()
。
- 將必要的遷移策略(migration strategy)新增至 builder。使用
.fallbackToDestructiveMigration()
。
一般來說,您必須為遷移物件(migration object)提供有關何時變更結構定義(schema)的遷移策略(migration strategy)。遷移物件(migration object)是一種 object,可定義如何擷取舊結構定義(schema)中的所有 rows,並將其轉換為新結構定義(schema)中的 rows,以免 data 遺失。遷移(migration)不在本程式碼研究室的範圍內。其中一個簡單的解決方法是刪除並重新 build database,但代表 data 會遺失。
1 | .fallbackToDestructiveMigration() |
- 如要建立 database instance,請呼叫
.build()
。這應該會移除 Android Studio 錯誤。
1 | .build() |
- 在
synchronized
區塊內,指派INSTANCE = instance
。
1 | INSTANCE = instance |
- 在
synchronized
區塊的結尾,returninstance
。
ItemRoomDatabase.kt
完整程式碼:
1 | // database class |
- 建構程式碼,確保沒有錯誤。
實作 Application class
在這項工作中,您需要在 Application class 中對 database instance 執行實例化(instantiate)。
- 開啟
InventoryApplication.kt
,建立 type 為ItemRoomDatabase
且名為database
的val
。 - 在傳入 context 的
ItemRoomDatabase
中呼叫getDatabase()
,藉此對database
instance 執行實例化(instantiate)。 - 請使用
lazy
委派(delegate),讓系統在您首次需要/存取 reference 時 (而非 app starts 時) 延遲建立database
instance。這項操作會在首次存取時建立 database (也就是磁碟(disk)上的實體(physical) database)。
- 開啟
1 | import android.app.Application |
- 稍後在程式碼研究室中建立
ViewModel
instance 時,您將用到這個database
。
現在您擁有了使用 Room
所需的所有 building 區塊。這個程式碼可以編譯並執行,但無法判斷它是否正常運作。因此,我們建議您在 Inventory database 中新增 new item 來測試 database。如要完成這項工作,您需要使用 ViewModel
與 database 互動。
新增 ViewModel
目前您建立了一個 database,且 UI class 屬於範例程式碼。如要儲存 app 的暫時性 data 並存取 database ,您必須具備 ViewModel
。 Inventory ViewModel 將透過 DAO 與 database 互動,並將 data 提供給 UI。所有 database 作業都必須透過 main UI thread 執行,為此,使用 coroutines 和 viewModelScope 即可。

建立 Inventory ViewModel
- 在
com.example.inventory
package 中,建立 Kotlin class 檔案InventoryViewModel.kt
。 InventoryViewModel
class 繼承ViewModel
class。將ItemDao
object 做為參數傳遞至預設 constructor。
1 | class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {} |
- 在 class 外的
InventoryViewModel.kt
檔案結尾,新增InventoryViewModelFactory
class,對InventoryViewModel
instance 執行實例化(instantiate)。傳入與做為ItemDao
instance 的InventoryViewModel
相同的 constructor 參數。繼承ViewModelProvider.Factory
class。您將在下一個步驟中修正未實作 method 的錯誤。
1 | class InventoryViewModelFactory(private val itemDao: ItemDao) : ViewModelProvider.Factory { |
- 按一下紅色燈泡並選取「Implement Members」,或是覆寫
ViewModelProvider.Factory
class 中的create()
method,這會將任何 class type 當做引數,然後 returnViewModel
object。
1 | override fun <T : ViewModel?> create(modelClass: Class<T>): T { |
- 實作
create()
method。檢查modelClass
是否和InventoryViewModel
class 相同,如果是,請 return 一個 instance。否則,您可以 throw 例外狀況(exception)。
1 | if (modelClass.isAssignableFrom(InventoryViewModel::class.java)) { |
填入 ViewModel
在這項工作中,您要填入 InventoryViewModel
class,將 inventory data 新增至 database。請查看 inventory app 中的 Item
entity 和「Add Item」畫面。
1 |
|

您需要該特定 item 的 name、price 和 stock,以便將 entity 新增到 database。在程式碼研究室的後續部分,您將使用「Add Item」畫面取得使用者的詳細資料(details)。在目前的工作中,您要:
- 使用三個 strings 做為
ViewModel
的 input。 - 將這些 strings 轉換為
Item
entity instance。 - 使用
ItemDao
instance 將其儲存至 database。
- 在
InventoryViewModel
class 中,新增名為insertItem()
的private
function,該 function 會擷取Item
object,並以非阻塞(non-blocking)的方式將 data 加入 database。
1 | private fun insertItem(item: Item) { |
- 如要透過 main thread 與 database 互動,請啟動 coroutine,然後呼叫其中的 DAO method。在
insertItem()
method 中,使用viewModelScope.launch
來啟動ViewModelScope
中的 coroutine。在啟動 function 中,對傳入item
的itemDao
呼叫 suspend functioninsert()
。ViewModelScope
是ViewModel
class 的 extension 屬性,會在ViewModel
刪除時自動取消其子項 coroutine。
1 | private fun insertItem(item: Item) { |
import kotlinx.coroutines.launch
和 androidx.lifecycle.viewModelScope
。
- 在
InventoryViewModel
class 中,新增另一個 private function,該 function 會擷取三個 strings,並 returnItem
instance。
1 | private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item { |
- 在
InventoryViewModel
class 中,新增名為addNewItem()
的 public function,該 function 會採用三個 strings 來取得 item detail strings。將 item detail strings 傳遞至getNewItemEntry()
function,並將 return 的值指派給名為newItem
的值。呼叫傳入newItem
的insertItem()
,以將 new entity 新增至 database。系統會從 UI fragment 中呼叫,將 Item details 新增至 database。
1 | fun addNewItem(itemName: String, itemPrice: String, itemCount: String) { |
請注意,您並沒有在 addNewItem()
中使用 viewModelScope.launch
,但呼叫 DAO method 時在上述 insertItem()
中會用到。這是因為系統只能從 coroutine 或其他 suspend function 中呼叫 suspend function,而 itemDao.insert(item)
是一種 suspend function。
InventoryViewModel.kt
完整程式碼:
1 | // view model |
您已新增所有必要的 function,可將 entity 新增至 database。在下一項工作中,您需要更新 Add Item fragment 以使用上述 function。
更新 AddItemFragment
- 在
AddItemFragment.kt
中AddItemFragment
class 的開頭,建立 type 為InventoryViewModel
且名為viewModel
的private val
。使用by activityViewModels()
Kotlin 屬性委派(delegate)功能,即可跨 fragments 共用(shared)ViewModel
。您將在下一個步驟中修正錯誤。
1 | private val viewModel: InventoryViewModel by activityViewModels { |
- 在
lambda
中,呼叫InventoryViewModelFactory()
constructor 並傳入ItemDao
instance。請使用先前其中一項工作中建立的database
instance,呼叫itemDao
constructor。
1 | private val viewModel: InventoryViewModel by activityViewModels { |
- 在
viewModel
定義下方,建立 type 為Item
且名為item
的lateinit var
。
1 | lateinit var item: Item |
- 「Add Item」畫面會顯示三個 text fields,用於取得使用者的 item details。在這個步驟中,您要新增 function,驗證
TextFields
中的 text 並非 empty。在新增或更新 database 中的 entity 之前,請使用這個 function 來驗證使用者輸入內容(input)。這項驗證程序必須在ViewModel
、而非 fragments 中進行。在InventoryViewModel
class 中,新增下列名為isEntryValid()
的public
function。
1 | fun isEntryValid(itemName: String, itemPrice: String, itemCount: String): Boolean { |
- 在
AddItemFragment.kt
的onCreateView()
function 下方,建立一個名為isEntryValid()
的private
function,該 function 會 returnBoolean
。您將在下一步中修正缺少 return 值這一錯誤。
1 | private fun isEntryValid(): Boolean { |
- 在
AddItemFragment
class 中,實作isEntryValid()
function。在viewModel
instance 上呼叫isEntryValid()
function,傳入 text views 中的 text。ReturnviewModel.isEntryValid()
function 的值。
1 | private fun isEntryValid(): Boolean { |
- 在
AddItemFragment
class 的isEntryValid()
function 下方,請新增另一個名為addNewItem()
的private
function,該 function 沒有參數且不會 return 任何內容。在 function 中,在if
條件內呼叫isEntryValid()
。
1 | private fun addNewItem() { |
- 在
if
區塊內,對viewModel
instance 呼叫addNewItem()
方法。傳入使用者輸入的 item details,使用binding
instance 讀取這些 data。
1 | if (isEntryValid()) { |
- 在
if
區塊下方建立val action
,導覽回(navigate back)ItemListFragment
。呼叫findNavController().navigate()
,以傳入action
。
1 | val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment() |
- 完整方法應如下所示。
1 | private fun addNewItem() { |
如要連結所有內容,請在 save button 中新增 click handler。在
AddItemFragment
class 的onDestroyView()
function 上方,覆寫onViewCreated()
function。在
onViewCreated()
function 中,將 click handler 新增至 save button,並呼叫addNewItem()
。
1 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
- build 並執行 app,然後輕觸「+」(FAB)。在「Add Item」畫面中,新增 item details,然後輕觸「Save」。這項操作會儲存 data,但 app 還不會顯示任何 data。在下一項工作中,您將使用 App Inspector 瀏覽已儲存的 data。


恭喜!您已經建立了一個 app,可使用 Room
保存 data。在接下來的程式碼研究室中,您將在 app 中新增 RecyclerView
以顯示 databse 中的 items,並在 app 中新增刪除(deleting)及更新(updating) entities 等功能。