1. 事前準備
大多數生產品質等級的應用程式都有需要儲存的資料,即便使用者關閉應用程式也不例外。舉例來說,應用程式可能會儲存歌曲播放清單、待辦事項清單、支出和收入記錄、星座目錄或個人資料記錄。在大部分情況下,您可以使用資料庫來儲存這些持續性資料。
Room 是 Android Jetpack 中的持續性程式庫。Room 是 SQLite 資料庫頂端的抽象層。SQLite 使用專門的語言 (SQL) 執行資料庫作業。Room 不直接使用 SQLite,因此簡化了設定資料庫並與之互動的過程。Room 也提供 SQLite 陳述式的編譯時間檢查。
下圖說明了 Room 如何配合本課程推薦的整體架構。
必要條件
- 您瞭解如何為 Android 應用程式建構基本的使用者介面 (UI)。
- 您瞭解如何使用活動、片段和檢視畫面。
- 你瞭解如何瀏覽於各個片段之間,使用 Safe Args 在片段之間傳遞資料。
- 您熟悉 Android 架構元件
ViewModel
、LiveData
和Flow
,也瞭解如何使用ViewModelProvider.Factory
將 ViewModels 執行個體化。 - 您熟悉並行的基礎知識。
- 您瞭解如何使用協同程式來處理長時間執行的工作。
- 您對 SQL 資料庫和 SQLite 語言有基本瞭解。
課程內容
- 如何使用 Room 程式庫建立 SQLite 資料庫並與之互動。
- 如何建立實體、DAO 和資料庫類別。
- 如何使用資料存取物件 (DAO) 將 Kotlin 函式對應至 SQL 查詢。
建構項目
- 您要建構一個商品目錄應用程式,用於將庫存商品儲存至 SQLite 資料庫。
需求條件
- Inventory 應用程式的範例程式碼。
- 已安裝 Android Studio 的電腦。
2. 應用程式總覽
在本程式碼研究室中,您將使用名為 Inventory 應用程式的範例應用程式,並透過 Room 程式庫將資料庫層加入其中。最終版本的應用程式會使用 RecyclerView
顯示商品目錄資料庫的商品清單。使用者可以選擇在商品目錄資料庫中新增商品、更新其中的現有商品,以及刪除其中的商品 (您將於下一個程式碼研究室完成應用程式功能)。
以下是最終版本應用程式的螢幕截圖。
3. 範例應用程式總覽
下載本程式碼研究室的範例程式碼
本程式碼研究室提供範例程式碼,可延伸至本程式碼研究室所教授的功能。範例程式碼可能包含程式碼研究室先前介紹過的程式碼,也可能會有之後才會介紹的程式碼,因此不盡然是您熟悉的內容。
如果您使用 GitHub 中的範例程式碼,請注意資料夾名稱是 android-basics-kotlin-inventory-app-starter
。在 Android Studio 中開啟專案時,請選取這個資料夾。
如要取得這個程式碼研究室的程式碼,並在 Android Studio 中開啟,請按照下列步驟操作:
取得程式碼
- 按一下所提供的網址。系統會在瀏覽器中開啟專案的 GitHub 頁面。
- 在專案的 GitHub 頁面中,按一下「Code」按鈕開啟對話方塊。
- 在對話方塊中,按一下「Download ZIP」按鈕,將專案儲存到電腦。等待下載作業完成。
- 在電腦上尋找該檔案 (可能位於「下載」資料夾中)。
- 按兩下 ZIP 檔案,將檔案解壓縮。這項操作會建立含有專案檔案的新資料夾。
在 Android Studio 中開啟專案
- 啟動 Android Studio。
- 在「Welcome to Android Studio」視窗中,按一下「Open an existing Android Studio project」。
注意:如果 Android Studio 已開啟,請依序選取「File」>「New」>「Import Project」選單選項。
- 在「Import Project」對話方塊中,前往解壓縮專案資料夾所在的位置 (可能位於「下載」資料夾中)。
- 按兩下該專案資料夾。
- 等待 Android Studio 開啟專案。
- 按一下「Run」按鈕 即可建構並執行應用程式。請確認應用程式的建構符合預期。
- 在「Project」工具視窗中瀏覽專案檔案,查看應用程式的設定方式。
範例程式碼總覽
- 在 Android Studio 中開啟含有範例程式碼的專案。
- 在 Android 裝置或模擬器上執行應用程式。請確保模擬器或已連結的裝置搭載 API 級別 26 或以上版本。資料庫檢查器最適合在搭載 API 級別 26 的模擬器/裝置上運作。
- 應用程式不會顯示商品目錄資料。請注意,使用懸浮動作按鈕 (FAB) 可將新的商品新增至資料庫。
- 按一下懸浮動作按鈕 (FAB)。應用程式將顯示新的畫面,您可以在該畫面中輸入新商品的詳細資料。
範例程式碼相關問題
- 在「Add Item」畫面中,輸入商品的詳細資料。輕觸「Save」(儲存)。新增商品片段未關閉。使用系統返回鍵返回。系統不會儲存新商品,也不會將其列在商品目錄畫面中。請注意,此應用程式並不完整,且未實作「Save」按鈕功能。
在本程式碼研究室中,您將新增應用程式的資料庫部分,該部分會將商品目錄詳細資料儲存至 SQLite 資料庫。您將使用 Room 持續性程式庫與 SQLite 資料庫互動。
程式碼逐步操作說明
您下載的範例程式碼包含已為您預先設計的螢幕版面配置。在本課程中,您將重點瞭解實作資料庫邏輯。以下是一些檔案的簡要逐步操作說明,協助您快速上手。
main_activity.xml
應用程式中代管所有其他片段的主要活動。onCreate()
方法會從 NavHostFragment
擷取 NavController
,並設定與 NavController
搭配使用的動作列。
item_list_fragment.xml
應用程式中顯示的第一個畫面。主要包含 RecyclerView 和懸浮動作按鈕 (FAB)。您會在稍後的課程中實作 RecyclerView。
fragment_add_item.xml
這個版面配置包含文字欄位,用於輸入要新增的新商品目錄商品的詳細資料。
ItemListFragment.kt
這個片段主要包含樣板程式碼。在 onViewCreated()
方法中,對懸浮動作按鈕 (FAB) 進行了設定,按一下事件監聽器即可前往新增商品片段。
AddItemFragment.kt
這個片段用於向資料庫新增商品。onCreateView()
函式會初始化繫結變數,onDestroyView()
函式則會在刪除片段前隱藏鍵盤。
4. Room 的主要元件
Kotlin 可透過引入資料類別,輕鬆處理資料。這些資料可存取,並可透過函式呼叫加以修改。但在資料庫中,您需要使用資料表和查詢來存取及修改資料。以下 Room 元件能讓這些工作流程順暢運作。
Room 有三個主要元件:
- 資料實體,代表應用程式資料庫中的資料表。它們可用於更新資料表中以資料列形式儲存的資料,也可用於建立要插入的新資料列。
- 資料存取物件 (DAO) 為應用程式提供了各種方法,用來擷取、更新、插入及刪除資料庫中的資料。
- 資料庫類別可存放資料庫,是應用程式資料庫基礎連線的主要存取點。資料庫類別為您的應用程式提供了與該資料庫關聯的 DAO 例項。
您將在稍後的程式碼研究室中實作這些元件,並進一步瞭解它們。下圖演示了 Room 的各元件如何協同工作以與資料庫互動。
新增 Room 程式庫
在這項工作中,您要將必要的 Room 元件程式庫加入 Gradle 檔案。
- 開啟模組層級的 Gradle 檔案
build.gradle (Module: InventoryApp.app)
。在dependencies
區塊中,為 Room 程式庫新增下列依附元件。
// Room implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$room_version"
5. 建立商品實體
實體類別定義了資料表,這個類別的所有例項都代表了資料庫資料表中的一個資料列。實體類別擁有對應項目,可用來向 Room 顯示它打算如何呈現資料庫中的資訊並與之互動。在應用程式中,這個實體會存放有關商品目錄商品的資訊,例如商品名稱、商品價格和庫存狀況。
@Entity
註解會將類別標示為資料庫實體類別。系統會為每個實體類別建立資料庫資料表,用於存放商品。在實體中,每個欄位都會表示為資料庫中的一列資料欄,除非另有說明 (詳情請參閱實體文件)。儲存在資料庫中的所有實體例項都必須有主鍵。主鍵用來唯一辨識資料庫資料表中的每條記錄/每個商品。主鍵一經指派即無法修改,只要存在於資料庫中,指的就是實體物件。
在這項工作中,您將建立實體類別。定義欄位以儲存每個商品的下列商品目錄資訊。
- 用於儲存主鍵的
Int
。 - 用於儲存商品名稱的
String
。 - 用於儲存商品價格的
double
。 - 用於儲存庫存數量的
Int
。
- 在 Android Studio 中開啟範例程式碼。
- 在
com.example.inventory
基本套件下方建立名為data
的套件。
- 在
data
套件中,建立名為Item
的 Kotlin 類別。這個類別將代表應用程式中的資料庫實體。在下一個步驟中,您將新增對應欄位來儲存商品目錄資訊。 - 使用下列程式碼更新
Item
類別定義。宣告Int
類型的id
、String,
類型的itemName
、Double
類型的itemPrice
,以及Int
類型的quantityInStock
做為主要建構函式的參數。將0
的預設值指派為id
。此值將成為主鍵,用來辨識Item
資料表中每條記錄/每個商品的專屬 ID。
class Item(
val id: Int = 0,
val itemName: String,
val itemPrice: Double,
val quantityInStock: Int
)
資料類別
資料類別主要用於保存 Kotlin 中的資料。這些類別標示有 data
這個關鍵字。Kotlin 資料類別物件有許多其他優勢,編譯器會自動產生公用程式,用於比較、列印及複製 toString()
、copy()
、equals()
等。
範例:
// Example data class with 2 properties.
data class User(val first_name: String, val last_name: String){
}
為了讓產生的程式碼保持一致且行為有意義,資料類別必須符合下列規定:
- 主要建構函式必須有至少一個參數。
- 所有主要建構函式參數都必須標示為
val
或var
。 - 資料類別不得為
abstract
、open
、sealed
或inner
。
如要進一步瞭解資料類別,請參閱說明文件。
- 在
Item
類別的類別定義前加上data
關鍵字,將其轉換為資料類別。
data class Item(
val id: Int = 0,
val itemName: String,
val itemPrice: Double,
val quantityInStock: Int
)
- 在
Item
類別宣告上方,為資料類別加上註解@Entity
。使用tableName
引數提供item
做為 SQLite 資料表名稱。
@Entity(tableName = "item")
data class Item(
...
)
- 要將
id
標識為主鍵,請為id
屬性加上註解@PrimaryKey
。將autoGenerate
參數設為true
,以便Room
為每個實體產生 ID。這能保證每個商品的 ID 都不重複。
@Entity(tableName = "item")
data class Item(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
...
)
- 為其餘屬性加上註解
@ColumnInfo
。ColumnInfo
註解可用來自訂與特定欄位相關的資料欄。舉例來說,使用name
引數時,您可以為欄位指定不同的資料欄名稱,而非變數名稱。使用參數自訂屬性名稱,如下所示。這種做法類似於使用tableName
為資料庫指定不同的名稱。
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Item(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "name")
val itemName: String,
@ColumnInfo(name = "price")
val itemPrice: Double,
@ColumnInfo(name = "quantity")
val quantityInStock: Int
)
6. 建立商品 DAO
資料存取物件 (DAO)
資料存取物件 (DAO) 是一種模式,可透過提供抽象介面,從應用程式的其餘部分中區隔出持久層。這種隔離機制符合單一責任原則,該原則在先前的程式碼研究室中有所提及。
DAO 的功能是向應用程式的其餘部分隱藏在基礎持續性層中執行資料庫作業所涉及的所有復雜性。這樣一來,就可以變更資料存取層,而不受使用該資料的程式碼影響。
在這項工作中,您要為 Room 定義資料存取物件 (DAO)。資料存取物件是 Room 的主要元件,負責定義存取資料庫的介面。
我們將建立的 DAO 是自訂介面,該介面可提供便利的方法,用於查詢/擷取、插入、刪除及更新資料庫。Room 會在編譯時間產生這個類別的實作。
針對常見的資料庫作業,Room
程式庫可提供便利的註解,例如 @Insert
、@Delete
和 @Update
。除此之外,您還可以使用 @Query
註解。您可以編寫受 SQLite 支援的任何查詢。
另一個好處是,當您在 Android Studio 中編寫查詢時,編譯器會檢查 SQL 查詢是否有語法錯誤。
對於商品目錄應用程式,您需要能夠執行以下操作:
- 插入或新增商品。
- 更新現有商品的名稱、價格和數量。
- 根據商品主鍵
id
取得特定商品。 - 取得所有商品,以便顯示它們。
- 刪除資料庫中的商品。
接著在應用程式中實作商品 DAO:
- 在
data
套件中建立 Kotlin 類別ItemDao.kt
。 - 將類別定義變更為
interface
,並加上註解@Dao
。
@Dao
interface ItemDao {
}
- 在介面內文中,新增
@Insert
註解。在@Insert
下方,新增insert()
函式,以將Entity
類別item
的例項做為引數。資料庫作業執行時間可能較長,因此應該會在另一個執行緒中執行。請將函式設為暫停函式,以便從協同程式中呼叫這個函式。
@Insert
suspend fun insert(item: Item)
- 新增引數
OnConflict
,並為其指派OnConflictStrategy.
IGNORE
的值。OnConflict
引數會指示 Room 在發生衝突時應如何處理。如果新商品的主鍵已存在於資料庫中,則OnConflictStrategy.
IGNORE
策略會忽略新商品。如要進一步瞭解可用的衝突策略,請參閱說明文件。
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)
現在,Room
會產生將 item
插入資料庫所需的所有程式碼。當您從 Kotlin 程式碼呼叫 insert()
時,Room
會執行 SQL 查詢,將實體插入資料庫中。(注意:您可以將函式命名為任何名稱,不一定要使用 insert()
)。
- 為一個
item
新增帶有update()
函式的@Update
註解。更新的實體與傳入的實體金鑰相同。您可以更新實體的部分或全部其他屬性。類似於insert()
方法,使以下update()
方法suspend
。
@Update
suspend fun update(item: Item)
- 新增具有
delete()
函式的@Delete
註解以刪除商品。使其成為停權方法。@Delete
註解會刪除一個商品或一個商品清單。(注意:您需要傳遞要刪除的實體;若您沒有實體,則可能要在呼叫delete()
函式之前擷取實體。)
@Delete
suspend fun delete(item: Item)
剩餘的函式沒有便利的註解,因此您必須使用 @Query
註解並提供 SQLite 查詢。
- 編寫 SQLite 查詢,根據指定的
id
從商品資料表中擷取特定商品。接著,您要新增 Room 註解,並在後續步驟中使用修改後的下列查詢。在後續步驟中,您還要透過 Room 將這項內容變更為 DAO 方法。 - 從
item
中選取所有欄 WHERE
id
符合特定值。
範例:
SELECT * from item WHERE id = 1
- 變更上述 SQL 查詢,使其與 Room 註解和引數搭配使用。新增
@Query
註解,將查詢以字串參數的形式提供給@Query
註解。將String
參數新增至@Query
,這是一個 SQLite 查詢,用於從商品資料表中擷取商品。 - 從
item
中選取所有欄 WHERE
id
與 :id
引數相符。請留意:id
。您可以在查詢中使用冒號標記法來參照函式中的引數。
@Query("SELECT * from item WHERE id = :id")
- 在
@Query
註解下方,新增getItem()
函式,這個函式會採用Int
引數並傳回Flow<Item>
。
@Query("SELECT * from item WHERE id = :id")
fun getItem(id: Int): Flow<Item>
使用 Flow
或 LiveData
做為傳回類型,可確保當資料庫中的資料有變更時,您會收到通知。建議您在持續性層中使用 Flow
。Room
將隨時為您更新這個 Flow
,因此您只需要明確取得資料一次即可。這有助於更新商品目錄清單,您將於下一個程式碼研究室中實作這部分內容。根據 Flow
傳回類型,Room 也會在背景執行緒上執行查詢。您不需要將它明確設為 suspend
函式,並在協同程式範圍內呼叫。
您可能需要從 kotlinx.coroutines.flow.Flow
匯入 Flow
。
- 新增具有
getItems()
函式的@Query
: - 讓 SQLite 查詢傳回
item
資料表中的所有資料欄,以遞增順序排序。 - 讓
getItems()
將Item
實體清單做為Flow
傳回。Room
將隨時為您更新這個Flow
,因此您只需要明確取得資料一次即可。
@Query("SELECT * from item ORDER BY name ASC")
fun getItems(): Flow<List<Item>>
- 雖然您看不到任何明顯的變更,但請執行應用程式,確定沒有任何錯誤。
7. 建立資料庫例項
在這項工作中,您要建立 RoomDatabase
,並使用您在先前工作中建立的 Entity
和 DAO。資料庫類別定義了實體清單和資料存取物件清單。同時也是基礎連線的主要存取點。
Database
類別為您的應用程式提供了定義的 DAO 例項。反過來,應用程式可以使用 DAO 來擷取資料庫中的資料,做為關聯資料實體物件的執行個體。應用程式也可以使用定義的資料實體,更新對應資料表中的資料列,或是建立新的資料列來插入資料。
您需要建立抽象的 RoomDatabase
類別,並加上 @Database
註解。此類別包含一個方法,可在 RoomDatabase
的例項不存在時建立該例項,或者傳回 RoomDatabase
的現有例項。
取得 RoomDatabase
例項的一般程序如下:
- 建立可擴充
RoomDatabase
的public abstract
類別。您定義的新抽象類別會成為資料庫容器。您定義的類別是抽象的,因為Room
會為您建立實作。 - 使用
@Database
為類別加上註解。在引數中,列出資料庫的實體並設定版本號碼。 - 定義傳回
ItemDao
例項的抽象方法或屬性,Room
會為您產生實作。 - 整個應用程式只需要一個
RoomDatabase
例項,因此請將RoomDatabase
設為單例模式。 - 僅在您的 (
item_database
) 資料庫不存在的情況下,使用Room
的Room.databaseBuilder
建立資料庫。否則,請傳回現有資料庫。
建立資料庫
- 在
data
套件中,建立 Kotlin 類別ItemRoomDatabase.kt
。 - 在
ItemRoomDatabase.kt
檔案中,將ItemRoomDatabase
類別設為可擴充RoomDatabase
的abstract
類別。使用@Database
為類別加上註解。您將在下一步中修正缺少參數的錯誤。
@Database
abstract class ItemRoomDatabase : RoomDatabase() {}
@Database
註解需要多個引數,以便Room
能夠建構資料庫。
- 將
Item
指定為包含entities
清單的唯一類別。 - 將
version
設為1
。每次變更資料庫資料表的結構定義時,都必須增加版本號碼。 - 只要將
exportSchema
設為false
,即可不保留結構定義版本記錄的備份。
@Database(entities = [Item::class], version = 1, exportSchema = false)
- 資料庫需要知道該 DAO。在類別內文中,宣告一個傳回
ItemDao
的抽象函式。您可以擁有多個 DAO。
abstract fun itemDao(): ItemDao
- 在抽象函式下方,定義
companion
物件。夥伴物件可讓您使用類別名稱做為限定詞,建立或取得資料庫。
companion object {}
- 在
companion
物件中,宣告資料庫的私人空值變數INSTANCE
,並將其初始化為null
。INSTANCE
變數會在建立資料庫時保留對該資料庫的參照。這有助於維護在指定時間開啟的資料庫單一例項,該例項是建立及維護成本很高的資源。
使用 @Volatile
為 INSTANCE
加上註解。系統一律不會快取易失變數的值,所有讀取與寫入作業都會在主記憶體中完成。這有助於確保所有執行執行緒的 INSTANCE
值保持在最新狀態且相同。這表示一個執行緒對 INSTANCE
所做的變更將立即對所有其他執行緒可見。
@Volatile
private var INSTANCE: ItemRoomDatabase? = null
- 在
INSTANCE
下方的companion
物件內,使用資料庫建構工具所需的Context
參數定義getDatabase()
方法。傳回類型ItemRoomDatabase
。由於getDatabase()
尚未傳回任何內容,因此系統會顯示錯誤訊息。
fun getDatabase(context: Context): ItemRoomDatabase {}
- 多個執行緒可能會產生競爭狀況,並同時要求一個資料庫例項,如此一來就會產生兩個資料庫,而非一個資料庫。納入程式碼以將資料庫納入
synchronized
區塊中,這表示一次只能執行一個執行緒,只有這個執行緒可以進入此程式碼區塊,從而確保系統只會將資料庫初始化一次。
在 getDatabase()
中,傳回 INSTANCE
變數;如果 INSTANCE
為空值,則在 synchronized{}
區塊內對其進行初始化。使用 elvis 運算子 (?:
) 執行此作業。傳入夥伴物件 this
,也就是要在函式區塊中鎖定的夥伴物件。您將在後續步驟中修正此錯誤。
return INSTANCE ?: synchronized(this) { }
- 在同步區塊中,建立
val
例項變數,並使用資料庫建構工具取得資料庫。您還有錯誤有待在後續步驟中修正。
val instance = Room.databaseBuilder()
- 在
synchronized
區塊的結尾,傳回instance
。
return instance
- 在
synchronized
區塊中,初始化instance
變數,並使用資料庫建構工具取得資料庫。將應用程式結構定義、資料庫類別以及資料庫的名稱item_database
傳遞給Room.databaseBuilder()
。
val instance = Room.databaseBuilder(
context.applicationContext,
ItemRoomDatabase::class.java,
"item_database"
)
Android Studio 會產生「類型不符」錯誤。如要移除這項錯誤,請按照下列步驟新增遷移策略和 build()
。
- 將必要的遷移策略新增至建構工具。使用
.fallbackToDestructiveMigration()
。
一般來說,您必須為遷移物件提供有關何時變更結構定義的遷移策略。遷移物件是一種物件,可定義如何擷取舊結構定義中的所有資料列,並將其轉換為新結構定義中的資料列,以免資料遺失。遷移不在本程式碼研究室的範圍內。其中一個簡單的解決方法是刪除並重新建構資料庫,但代表資料會遺失。
.fallbackToDestructiveMigration()
- 如要建立資料庫例項,請呼叫
.build()
。這應該會移除 Android Studio 錯誤。
.build()
- 在
synchronized
區塊內,指派INSTANCE = instance
。
INSTANCE = instance
- 在
synchronized
區塊的結尾,傳回instance
。最終程式碼應如下所示:
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class ItemRoomDatabase : RoomDatabase() {
abstract fun itemDao(): ItemDao
companion object {
@Volatile
private var INSTANCE: ItemRoomDatabase? = null
fun getDatabase(context: Context): ItemRoomDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
ItemRoomDatabase::class.java,
"item_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
return instance
}
}
}
}
- 建構程式碼,確保沒有錯誤。
實作應用程式類別
在這項工作中,您需要在應用程式類別中對資料庫例項執行個體化。
- 開啟
InventoryApplication.kt
,建立類型為ItemRoomDatabase
且名為database
的val
。在傳入結構定義的ItemRoomDatabase
中呼叫getDatabase()
,藉此對database
例項執行個體化。請使用lazy
委派,讓系統在您首次需要/存取參照時 (而非應用程式啟動時) 延遲建立database
例項。這項操作會在首次存取時建立資料庫 (也就是磁碟上的實體資料庫)。
import android.app.Application
import com.example.inventory.data.ItemRoomDatabase
class InventoryApplication : Application(){
val database: ItemRoomDatabase by lazy { ItemRoomDatabase.getDatabase(this) }
}
稍後在程式碼研究室中建立 ViewModel 例項時,您將用到這個 database
。
現在您擁有了使用 Room 所需的所有建構區塊。這個程式碼可以編譯並執行,但無法判斷它是否正常運作。因此,我們建議您在商品目錄資料庫中新增商品來測試資料庫。如要完成這項工作,您需要使用 ViewModel
與資料庫互動。
8. 新增 ViewModel
目前您建立了一個資料庫,且使用者介面類別屬於範例程式碼。如要儲存應用程式的暫時性資料並存取資料庫,您必須具備 ViewModel。Inventory ViewModel 將透過 DAO 與資料庫互動,並將資料提供給使用者介面。所有資料庫作業都必須透過主使用者介面執行緒執行,為此,使用協同程式和 viewModelScope
即可。
建立商品目錄 ViewModel
- 在
com.example.inventory
套件中,建立 Kotlin 類別檔案InventoryViewModel.kt
。 - 從
ViewModel
類別擴充InventoryViewModel
類別。將ItemDao
物件做為參數傳遞至預設建構函式。
class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {}
- 在類別外的
InventoryViewModel.kt
檔案結尾,新增InventoryViewModelFactory
類別,對InventoryViewModel
執行個體執行個體化。傳入與做為ItemDao
例項的InventoryViewModel
相同的建構函式參數。使用ViewModelProvider.Factory
類別來擴充該類別。您將在下一個步驟中修正未實作方法的錯誤。
class InventoryViewModelFactory(private val itemDao: ItemDao) : ViewModelProvider.Factory {
}
- 按一下紅色燈泡並選取「Implement Members」,或是覆寫
ViewModelProvider.Factory
類別中的create()
方法 (如下所示),這會將任何類別類型當做引數,然後傳回ViewModel
物件。
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
TODO("Not yet implemented")
}
- 實作
create()
方法。檢查modelClass
是否和InventoryViewModel
類別相同,如果是,請傳回一個例項。否則,您可以擲回例外狀況。
if (modelClass.isAssignableFrom(InventoryViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return InventoryViewModel(itemDao) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
填入 ViewModel
在這項工作中,您要填入 InventoryViewModel
類別,將商品目錄資料新增至資料庫。請查看商品目錄應用程式中的 Item
實體和「Add Item」畫面。
@Entity
data class Item(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "name")
val itemName: String,
@ColumnInfo(name = "price")
val itemPrice: Double,
@ColumnInfo(name = "quantity")
val quantityInStock: Int
)
您需要該特定商品的名稱、價格和庫存,以便將實體新增到資料庫。在程式碼研究室的後續部分,您將使用「Add Item」畫面取得使用者的詳細資料。在目前的工作中,您要使用三個字串做為 ViewModel 的輸入內容、將這些字串轉換為 Item
實體例項,並使用 ItemDao
例項將其儲存至資料庫。現在可以開始實作了。
- 在
InventoryViewModel
類別中,新增名為insertItem()
的private
函式,該函式會擷取Item
物件,並以非封鎖的方式將資料加入資料庫。
private fun insertItem(item: Item) {
}
- 如要透過主執行緒與資料庫互動,請啟動協同程式,然後呼叫其中的 DAO 方法。在
insertItem()
方法中,使用viewModelScope.launch
來啟動ViewModelScope
中的協同程式。在啟動函式中,對傳入item
的itemDao
呼叫暫停函式insert()
。ViewModelScope
是ViewModel
類別的擴充功能屬性,會在ViewModel
刪除時自動取消其子項協同程式。
private fun insertItem(item: Item) {
viewModelScope.launch {
itemDao.insert(item)
}
}
匯入 kotlinx.coroutines.launch,
androidx.lifecycle.
viewModelScope
com.example.inventory.data.Item
(如未自動匯入)。
- 在
InventoryViewModel
類別中,新增另一個私人函式,該函式會擷取三個字串,並傳回Item
例項。
private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item {
return Item(
itemName = itemName,
itemPrice = itemPrice.toDouble(),
quantityInStock = itemCount.toInt()
)
}
- 在
InventoryViewModel
類別中,新增名為addNewItem()
的公開函式,該函式會採用三個字串來取得商品詳細資料。將商品詳細資料字串傳遞至getNewItemEntry()
函式,並將傳回的值指派給名為newItem
的值。呼叫傳入newItem
的insertItem()
,以將新實體新增至資料庫。系統會從使用者介面片段中呼叫,將商品詳細資料新增至資料庫。
fun addNewItem(itemName: String, itemPrice: String, itemCount: String) {
val newItem = getNewItemEntry(itemName, itemPrice, itemCount)
insertItem(newItem)
}
請注意,您並沒有在 addNewItem()
中使用 viewModelScope.launch
,但呼叫 DAO 方法時在上述 insertItem()
中會用到。這是因為系統只能從協同程式或其他暫停函式呼叫暫停函式。函式 itemDao.insert(item)
是一種暫停函式。
您已新增所有必要的函式,可將實體新增至資料庫。在下一項工作中,您需要更新新增商品片段以使用上述函式。
9. 更新 AddItemFragment
- 在
AddItemFragment.kt
中AddItemFragment
類別的開頭,建立類型為InventoryViewModel
且名為viewModel
的private val
。使用by activityViewModels()
Kotlin 屬性委派功能,即可跨片段共用ViewModel
。您將在下一個步驟中修正錯誤。
private val viewModel: InventoryViewModel by activityViewModels {
}
- 在 lambda 中,呼叫
InventoryViewModelFactory()
建構函式並傳入ItemDao
例項。請使用先前其中一項工作中建立的database
例項,呼叫itemDao
建構函式。
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database
.itemDao()
)
}
- 在
viewModel
定義下方,建立類型為Item
且名為item
的lateinit var
。
lateinit var item: Item
- 「Add Item」畫面會顯示三個文字欄位,用於取得使用者的商品詳細資料。在這個步驟中,您要新增函式,驗證 TextFields 中的文字並非空白。在新增或更新資料庫中的實體之前,請使用這個函式來驗證使用者輸入內容。這項驗證程序必須在
ViewModel
、而非片段中進行。在InventoryViewModel
類別中,新增下列名為isEntryValid()
的public
函式。
fun isEntryValid(itemName: String, itemPrice: String, itemCount: String): Boolean {
if (itemName.isBlank() || itemPrice.isBlank() || itemCount.isBlank()) {
return false
}
return true
}
- 在
AddItemFragment.kt
的onCreateView()
函式下方,建立一個名為isEntryValid()
的private
函式,該函式會傳回Boolean
。您將在下一步中修正缺少傳回值這一錯誤。
private fun isEntryValid(): Boolean {
}
- 在
AddItemFragment
類別中,實作isEntryValid()
函式。在viewModel
執行個體上呼叫isEntryValid()
函式,傳入文字檢視區塊中的文字。傳回viewModel.isEntryValid()
函式的值。
private fun isEntryValid(): Boolean {
return viewModel.isEntryValid(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString()
)
}
- 在
AddItemFragment
類別的isEntryValid()
函式下方,請新增另一個名為addNewItem()
的private
函式,該函式沒有參數且不會傳回任何內容。在函式中,在if
條件內呼叫isEntryValid()
。
private fun addNewItem() {
if (isEntryValid()) {
}
}
- 在
if
區塊內,對viewModel
例項呼叫addNewItem()
方法。傳入使用者輸入的商品詳細資料,使用binding
例項讀取這些資料。
if (isEntryValid()) {
viewModel.addNewItem(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString(),
)
}
- 在
if
區塊下方建立val
action
,返回ItemListFragment
。呼叫findNavController
().navigate()
,以傳入action
。
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)
匯入 androidx.navigation.fragment.findNavController.
- 完整方法應如下所示。
private fun addNewItem() {
if (isEntryValid()) {
viewModel.addNewItem(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString(),
)
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)
}
}
- 如要連結所有內容,請在「Save」按鈕中新增點擊處理常式。在
AddItemFragment
類別的onDestroyView()
函式上方,覆寫onViewCreated()
函式。 - 在
onViewCreated()
函式中,將點按處理常式新增至儲存按鈕,並呼叫addNewItem()
。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.saveAction.setOnClickListener {
addNewItem()
}
}
- 建構並執行應用程式,然後輕觸「+」懸浮動作按鈕 (FAB)。在「Add Item」畫面中,新增商品詳細資料,然後輕觸「Save」。這項操作會儲存資料,但應用程式還不會顯示任何資料。在下一項工作中,您將使用資料庫檢查器瀏覽已儲存的資料。
使用資料庫檢查器查看資料庫
- 在執行 API 級別 26 或以上的模擬器或已連結的裝置上執行應用程式 (如果您尚未這麼做)。資料庫檢查器最適合在搭載 API 級別 26 的模擬器/裝置上運作。
- 在 Android Studio 中,從選單列中選取「View」(檢視畫面) >「Tool Windows」(工具視窗) >「Database Inspector」(資料庫檢查器)。
- 在「Database Inspector」窗格中,從下拉式選單中選取
com.example.inventory
。 - 商品目錄應用程式中的「item_database」會顯示在「Databases」窗格中。展開「item_database」的節點,並選取「Item」進行檢查。如果「Databases」窗格沒有任何內容,請使用模擬器的「Add Item」畫面,將某些商品新增至資料庫。
- 勾選資料庫檢查器中的「Live updates」核取方塊,在您與模擬器或裝置上執行的應用程式互動時,自動更新系統顯示的資料。
恭喜!您已經建立了一個應用程式,可使用 Room 保存資料。在接下來的程式碼研究室中,您將在應用程式中新增 RecyclerView
以顯示資料庫中的商品,並在應用程式中新增刪除及更新實體等功能。到時見!
10. 解決方案程式碼
本程式碼研究室的解決方案程式碼位於以下所示的 GitHub 存放區和分支版本中。
如要取得這個程式碼研究室的程式碼,並在 Android Studio 中開啟,請按照下列步驟操作:
取得程式碼
- 按一下所提供的網址。系統會在瀏覽器中開啟專案的 GitHub 頁面。
- 在專案的 GitHub 頁面中,按一下「Code」按鈕開啟對話方塊。
- 在對話方塊中,按一下「Download ZIP」按鈕,將專案儲存到電腦。等待下載作業完成。
- 在電腦上尋找該檔案 (可能位於「下載」資料夾中)。
- 按兩下 ZIP 檔案,將檔案解壓縮。這項操作會建立含有專案檔案的新資料夾。
在 Android Studio 中開啟專案
- 啟動 Android Studio。
- 在「Welcome to Android Studio」視窗中,按一下「Open an existing Android Studio project」。
注意:如果 Android Studio 已開啟,請依序選取「File」>「New」>「Import Project」選單選項。
- 在「Import Project」對話方塊中,前往解壓縮專案資料夾所在的位置 (可能位於「下載」資料夾中)。
- 按兩下該專案資料夾。
- 等待 Android Studio 開啟專案。
- 按一下「Run」按鈕 即可建構並執行應用程式。請確認應用程式的建構符合預期。
- 在「Project」工具視窗中瀏覽專案檔案,查看應用程式的設定方式。
11. 摘要
- 將資料表定義為具有
@Entity
註解的資料類別。定義具有@ColumnInfo
註解的屬性做為資料表中的資料欄。 - 定義資料存取物件 (DAO) 做為標有
@Dao
註解的介面。DAO 會將 Kotlin 函式對應至資料庫查詢。 - 使用註解來定義
@Insert
、@Delete
和@Update
函式。 - 針對其他查詢,使用
@Query
註解搭配 SQLite 查詢字串做為參數。 - 使用資料庫檢查器查看儲存在 Android SQLite 資料庫中的資料。
12. 瞭解詳情
Android 開發人員說明文件
網誌文章
影片
其他說明文件和文章