建構一個 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)至AddItemFragmentAddItemFragment.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。
在
datapackage 中,建立名為Item的 Kotlin class。這個 class 將代表 app 中的 database entity。在下一個步驟中,您將新增對應欄位(fields)來儲存 inventory information。使用下列程式碼更新
Itemclass 定義。宣告Inttype 的id、Stringtype 的itemName、Doubletype 的itemPrice,以及Inttype 的quantityInStock做為 primary constructor 的參數。將id的預設值設為0。id將成為 primary key,用來辨識Itemtable 中每個 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,請參閱說明文件。
- 在
Itemclass 的 class 定義前加上datakeyword,將其轉換為 data class。
1 | data class Item( |
- 在
Itemclass 宣告上方,為 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:
- 在
datapackage 中建立 Kotlin classItemDao.kt。 - 將 class 定義變更為 interface,並加上註解
@Dao。
1 |
|
- 在 interface body 中,新增
@Insert註解。在@Insert下方,新增insert()function,以將Entityclassitem的 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 會產生將
iteminsert 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。 WHEREid符合特定值。
範例:
1 | SELECT * from item WHERE id = 1 |
- 變更上述 SQL 查詢(query),使其與 Room 註解和引數搭配使用。新增
@Query註解,將 query 以 string 參數的形式提供給@Query註解。將String參數新增至@Query,這是一個 SQLite 查詢(query),用於從 item table 中擷取 item。 - 從
item中選取所有 columns。 WHEREid=: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
itemtable 中的所有 columns,以 name 遞增順序排序。 - 使
getItems()returnItementities 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 abstractclass。您定義的新 abstract class 會成為 database holder。您定義的 class 為 abstract,因為Room會為您建立實作。 - 使用
@Database為 class 加上註解。在引數中,列出 database 的 entity 並設定 version number。 - 定義 return
ItemDaoinstance 的 abstract method 或屬性(property),Room會為您產生實作。 - 整個 app 只需要一個
RoomDatabaseinstance,因此請將RoomDatabase設為單例模式(singleton)。 - 僅在您的 (
item_database) database 不存在的情況下,使用 Room 的Room.databaseBuilder建立 database。否則,請 return 現有 database。
筆記:單例模式(singleton)可確保只建立一個 instance。Companion object 只會存在一個 instance,所以也屬於單例模式(singleton)。
建立 Database
- 在
datapackage 中,建立 Kotlin classItemRoomDatabase.kt。 - 在
ItemRoomDatabase.kt檔案中,將ItemRoomDatabaseclass 設為繼承RoomDatabase的abstractclass。使用@Database為 class 加上註解。您將在下一步中修正缺少參數的錯誤。
1 |
|
@Database註解需要多個引數,以便Room能夠 build database。
- 將
Item指定為包含entitieslist 的唯一 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區塊中,建立valinstance 變數,並使用 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(),藉此對databaseinstance 執行實例化(instantiate)。 - 請使用
lazy委派(delegate),讓系統在您首次需要/存取 reference 時 (而非 app starts 時) 延遲建立databaseinstance。這項操作會在首次存取時建立 database (也就是磁碟(disk)上的實體(physical) database)。
- 開啟
1 | import android.app.Application |
- 稍後在程式碼研究室中建立
ViewModelinstance 時,您將用到這個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.inventorypackage 中,建立 Kotlin class 檔案InventoryViewModel.kt。 InventoryViewModelclass 繼承ViewModelclass。將ItemDaoobject 做為參數傳遞至預設 constructor。
1 | class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {} |
- 在 class 外的
InventoryViewModel.kt檔案結尾,新增InventoryViewModelFactoryclass,對InventoryViewModelinstance 執行實例化(instantiate)。傳入與做為ItemDaoinstance 的InventoryViewModel相同的 constructor 參數。繼承ViewModelProvider.Factoryclass。您將在下一個步驟中修正未實作 method 的錯誤。
1 | class InventoryViewModelFactory(private val itemDao: ItemDao) : ViewModelProvider.Factory { |
- 按一下紅色燈泡並選取「Implement Members」,或是覆寫
ViewModelProvider.Factoryclass 中的create()method,這會將任何 class type 當做引數,然後 returnViewModelobject。
1 | override fun <T : ViewModel?> create(modelClass: Class<T>): T { |
- 實作
create()method。檢查modelClass是否和InventoryViewModelclass 相同,如果是,請 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 轉換為
Itementity instance。 - 使用
ItemDaoinstance 將其儲存至 database。
- 在
InventoryViewModelclass 中,新增名為insertItem()的privatefunction,該 function 會擷取Itemobject,並以非阻塞(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是ViewModelclass 的 extension 屬性,會在ViewModel刪除時自動取消其子項 coroutine。
1 | private fun insertItem(item: Item) { |
import kotlinx.coroutines.launch 和 androidx.lifecycle.viewModelScope。
- 在
InventoryViewModelclass 中,新增另一個 private function,該 function 會擷取三個 strings,並 returnIteminstance。
1 | private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item { |
- 在
InventoryViewModelclass 中,新增名為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中AddItemFragmentclass 的開頭,建立 type 為InventoryViewModel且名為viewModel的private val。使用by activityViewModels()Kotlin 屬性委派(delegate)功能,即可跨 fragments 共用(shared)ViewModel。您將在下一個步驟中修正錯誤。
1 | private val viewModel: InventoryViewModel by activityViewModels { |
- 在
lambda中,呼叫InventoryViewModelFactory()constructor 並傳入ItemDaoinstance。請使用先前其中一項工作中建立的databaseinstance,呼叫itemDaoconstructor。
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 中進行。在InventoryViewModelclass 中,新增下列名為isEntryValid()的publicfunction。
1 | fun isEntryValid(itemName: String, itemPrice: String, itemCount: String): Boolean { |
- 在
AddItemFragment.kt的onCreateView()function 下方,建立一個名為isEntryValid()的privatefunction,該 function 會 returnBoolean。您將在下一步中修正缺少 return 值這一錯誤。
1 | private fun isEntryValid(): Boolean { |
- 在
AddItemFragmentclass 中,實作isEntryValid()function。在viewModelinstance 上呼叫isEntryValid()function,傳入 text views 中的 text。ReturnviewModel.isEntryValid()function 的值。
1 | private fun isEntryValid(): Boolean { |
- 在
AddItemFragmentclass 的isEntryValid()function 下方,請新增另一個名為addNewItem()的privatefunction,該 function 沒有參數且不會 return 任何內容。在 function 中,在if條件內呼叫isEntryValid()。
1 | private fun addNewItem() { |
- 在
if區塊內,對viewModelinstance 呼叫addNewItem()方法。傳入使用者輸入的 item details,使用bindinginstance 讀取這些 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。在
AddItemFragmentclass 的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 等功能。