使用 Room 保存資料

1. 事前準備

大多數生產品質等級的應用程式都有需要儲存的資料,即便使用者關閉應用程式也不例外。舉例來說,應用程式可能會儲存歌曲播放清單、待辦事項清單、支出和收入記錄、星座目錄或個人資料記錄。在大部分情況下,您可以使用資料庫來儲存這些持續性資料。

Room 是 Android Jetpack 中的持續性程式庫。Room 是 SQLite 資料庫頂端的抽象層。SQLite 使用專門的語言 (SQL) 執行資料庫作業。Room 不直接使用 SQLite,因此簡化了設定資料庫並與之互動的過程。Room 也提供 SQLite 陳述式的編譯時間檢查。

下圖說明了 Room 如何配合本課程推薦的整體架構。

7521165e051cc0d4.png

必要條件

  • 您瞭解如何為 Android 應用程式建構基本的使用者介面 (UI)。
  • 您瞭解如何使用活動、片段和檢視畫面。
  • 你瞭解如何瀏覽於各個片段之間,使用 Safe Args 在片段之間傳遞資料。
  • 您熟悉 Android 架構元件 ViewModelLiveDataFlow,也瞭解如何使用 ViewModelProvider.Factory 將 ViewModels 執行個體化。
  • 您熟悉並行的基礎知識。
  • 您瞭解如何使用協同程式來處理長時間執行的工作。
  • 您對 SQL 資料庫和 SQLite 語言有基本瞭解。

課程內容

  • 如何使用 Room 程式庫建立 SQLite 資料庫並與之互動。
  • 如何建立實體、DAO 和資料庫類別。
  • 如何使用資料存取物件 (DAO) 將 Kotlin 函式對應至 SQL 查詢。

建構項目

  • 您要建構一個商品目錄應用程式,用於將庫存商品儲存至 SQLite 資料庫。

需求條件

  • Inventory 應用程式的範例程式碼。
  • 已安裝 Android Studio 的電腦。

2. 應用程式總覽

在本程式碼研究室中,您將使用名為 Inventory 應用程式的範例應用程式,並透過 Room 程式庫將資料庫層加入其中。最終版本的應用程式會使用 RecyclerView 顯示商品目錄資料庫的商品清單。使用者可以選擇在商品目錄資料庫中新增商品、更新其中的現有商品,以及刪除其中的商品 (您將於下一個程式碼研究室完成應用程式功能)。

以下是最終版本應用程式的螢幕截圖。

439ad9a8183278c5.png

3. 範例應用程式總覽

下載本程式碼研究室的範例程式碼

本程式碼研究室提供範例程式碼,可延伸至本程式碼研究室所教授的功能。範例程式碼可能包含程式碼研究室先前介紹過的程式碼,也可能會有之後才會介紹的程式碼,因此不盡然是您熟悉的內容。

如果您使用 GitHub 中的範例程式碼,請注意資料夾名稱是 android-basics-kotlin-inventory-app-starter。在 Android Studio 中開啟專案時,請選取這個資料夾。

如要取得這個程式碼研究室的程式碼,並在 Android Studio 中開啟,請按照下列步驟操作:

取得程式碼

  1. 按一下所提供的網址。系統會在瀏覽器中開啟專案的 GitHub 頁面。
  2. 在專案的 GitHub 頁面中,按一下「Code」按鈕開啟對話方塊。

5b0a76c50478a73f.png

  1. 在對話方塊中,按一下「Download ZIP」按鈕,將專案儲存到電腦。等待下載作業完成。
  2. 在電腦上尋找該檔案 (可能位於「下載」資料夾中)。
  3. 按兩下 ZIP 檔案,將檔案解壓縮。這項操作會建立含有專案檔案的新資料夾。

在 Android Studio 中開啟專案

  1. 啟動 Android Studio。
  2. 在「Welcome to Android Studio」視窗中,按一下「Open an existing Android Studio project」

36cc44fcf0f89a1d.png

注意:如果 Android Studio 已開啟,請依序選取「File」>「New」>「Import Project」選單選項。

21f3eec988dcfbe9.png

  1. 在「Import Project」對話方塊中,前往解壓縮專案資料夾所在的位置 (可能位於「下載」資料夾中)。
  2. 按兩下該專案資料夾。
  3. 等待 Android Studio 開啟專案。
  4. 按一下「Run」按鈕 11c34fc5e516fb1c.png 即可建構並執行應用程式。請確認應用程式的建構符合預期。
  5. 在「Project」工具視窗中瀏覽專案檔案,查看應用程式的設定方式。

範例程式碼總覽

  1. 在 Android Studio 中開啟含有範例程式碼的專案。
  2. 在 Android 裝置或模擬器上執行應用程式。請確保模擬器或已連結的裝置搭載 API 級別 26 或以上版本。資料庫檢查器最適合在搭載 API 級別 26 的模擬器/裝置上運作。
  3. 應用程式不會顯示商品目錄資料。請注意,使用懸浮動作按鈕 (FAB) 可將新的商品新增至資料庫。
  4. 按一下懸浮動作按鈕 (FAB)。應用程式將顯示新的畫面,您可以在該畫面中輸入新商品的詳細資料。

9c5e361a89453821.png

範例程式碼相關問題

  1. 在「Add Item」畫面中,輸入商品的詳細資料。輕觸「Save」(儲存)。新增商品片段未關閉。使用系統返回鍵返回。系統不會儲存新商品,也不會將其列在商品目錄畫面中。請注意,此應用程式並不完整,且未實作「Save」按鈕功能。

f0931dab5089a14f.png

在本程式碼研究室中,您將新增應用程式的資料庫部分,該部分會將商品目錄詳細資料儲存至 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 的各元件如何協同工作以與資料庫互動。

33a193a68c9a8e0e.png

新增 Room 程式庫

在這項工作中,您要將必要的 Room 元件程式庫加入 Gradle 檔案。

  1. 開啟模組層級的 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 顯示它打算如何呈現資料庫中的資訊並與之互動。在應用程式中,這個實體會存放有關商品目錄商品的資訊,例如商品名稱、商品價格和庫存狀況。

8c9f1659ee82ca43.png

@Entity 註解會將類別標示為資料庫實體類別。系統會為每個實體類別建立資料庫資料表,用於存放商品。在實體中,每個欄位都會表示為資料庫中的一列資料欄,除非另有說明 (詳情請參閱實體文件)。儲存在資料庫中的所有實體例項都必須有主鍵。主鍵用來唯一辨識資料庫資料表中的每條記錄/每個商品。主鍵一經指派即無法修改,只要存在於資料庫中,指的就是實體物件。

在這項工作中,您將建立實體類別。定義欄位以儲存每個商品的下列商品目錄資訊。

  • 用於儲存主鍵的 Int
  • 用於儲存商品名稱的 String
  • 用於儲存商品價格的 double
  • 用於儲存庫存數量的 Int
  1. 在 Android Studio 中開啟範例程式碼。
  2. com.example.inventory 基本套件下方建立名為 data 的套件。

be39b42484ba2664.png

  1. data 套件中,建立名為 Item 的 Kotlin 類別。這個類別將代表應用程式中的資料庫實體。在下一個步驟中,您將新增對應欄位來儲存商品目錄資訊。
  2. 使用下列程式碼更新 Item 類別定義。宣告 Int 類型的 idString, 類型的 itemNameDouble 類型的 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){
}

為了讓產生的程式碼保持一致且行為有意義,資料類別必須符合下列規定:

  • 主要建構函式必須有至少一個參數。
  • 所有主要建構函式參數都必須標示為 valvar
  • 資料類別不得為 abstractopensealedinner

如要進一步瞭解資料類別,請參閱說明文件

  1. Item 類別的類別定義前加上 data 關鍵字,將其轉換為資料類別。
data class Item(
   val id: Int = 0,
   val itemName: String,
   val itemPrice: Double,
   val quantityInStock: Int
)
  1. Item 類別宣告上方,為資料類別加上註解 @Entity。使用 tableName 引數提供 item 做為 SQLite 資料表名稱。
@Entity(tableName = "item")
data class Item(
   ...
)
  1. 要將 id 標識為主鍵,請為 id 屬性加上註解 @PrimaryKey。將 autoGenerate 參數設為 true,以便 Room 為每個實體產生 ID。這能保證每個商品的 ID 都不重複。
@Entity(tableName = "item")
data class Item(
   @PrimaryKey(autoGenerate = true)
   val id: Int = 0,
   ...
)
  1. 為其餘屬性加上註解 @ColumnInfoColumnInfo 註解可用來自訂與特定欄位相關的資料欄。舉例來說,使用 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 的功能是向應用程式的其餘部分隱藏在基礎持續性層中執行資料庫作業所涉及的所有復雜性。這樣一來,就可以變更資料存取層,而不受使用該資料的程式碼影響。

7a8480711f04b3ef.png

在這項工作中,您要為 Room 定義資料存取物件 (DAO)。資料存取物件是 Room 的主要元件,負責定義存取資料庫的介面。

我們將建立的 DAO 是自訂介面,該介面可提供便利的方法,用於查詢/擷取、插入、刪除及更新資料庫。Room 會在編譯時間產生這個類別的實作。

針對常見的資料庫作業,Room 程式庫可提供便利的註解,例如 @Insert@Delete@Update。除此之外,您還可以使用 @Query 註解。您可以編寫受 SQLite 支援的任何查詢。

另一個好處是,當您在 Android Studio 中編寫查詢時,編譯器會檢查 SQL 查詢是否有語法錯誤。

對於商品目錄應用程式,您需要能夠執行以下操作:

  • 插入或新增商品。
  • 更新現有商品的名稱、價格和數量。
  • 根據商品主鍵 id 取得特定商品。
  • 取得所有商品,以便顯示它們。
  • 刪除資料庫中的商品。

bb381857d5fba511.png

接著在應用程式中實作商品 DAO:

  1. data 套件中建立 Kotlin 類別 ItemDao.kt
  2. 將類別定義變更為 interface,並加上註解 @Dao
@Dao
interface ItemDao {
}
  1. 在介面內文中,新增 @Insert 註解。在 @Insert 下方,新增 insert() 函式,以將 Entity 類別 item 的例項做為引數。資料庫作業執行時間可能較長,因此應該會在另一個執行緒中執行。請將函式設為暫停函式,以便從協同程式中呼叫這個函式。
@Insert
suspend fun insert(item: Item)
  1. 新增引數 OnConflict,並為其指派 OnConflictStrategy.IGNORE 的值。OnConflict 引數會指示 Room 在發生衝突時應如何處理。如果新商品的主鍵已存在於資料庫中,則 OnConflictStrategy.IGNORE 策略會忽略新商品。如要進一步瞭解可用的衝突策略,請參閱說明文件
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)

現在,Room 會產生將 item 插入資料庫所需的所有程式碼。當您從 Kotlin 程式碼呼叫 insert() 時,Room 會執行 SQL 查詢,將實體插入資料庫中。(注意:您可以將函式命名為任何名稱,不一定要使用 insert())。

  1. 為一個 item 新增帶有 update() 函式的 @Update 註解。更新的實體與傳入的實體金鑰相同。您可以更新實體的部分或全部其他屬性。類似於 insert() 方法,使以下 update() 方法 suspend
@Update
suspend fun update(item: Item)
  1. 新增具有 delete() 函式的 @Delete 註解以刪除商品。使其成為停權方法。@Delete 註解會刪除一個商品或一個商品清單。(注意:您需要傳遞要刪除的實體;若您沒有實體,則可能要在呼叫 delete() 函式之前擷取實體。)
@Delete
suspend fun delete(item: Item)

剩餘的函式沒有便利的註解,因此您必須使用 @Query 註解並提供 SQLite 查詢。

  1. 編寫 SQLite 查詢,根據指定的 id 從商品資料表中擷取特定商品。接著,您要新增 Room 註解,並在後續步驟中使用修改後的下列查詢。在後續步驟中,您還要透過 Room 將這項內容變更為 DAO 方法。
  2. item 中選取所有欄
  3. WHERE id 符合特定值。

範例:

SELECT * from item WHERE id = 1
  1. 變更上述 SQL 查詢,使其與 Room 註解和引數搭配使用。新增 @Query 註解,將查詢以字串參數的形式提供給 @Query 註解。將 String 參數新增至 @Query,這是一個 SQLite 查詢,用於從商品資料表中擷取商品。
  2. item 中選取所有欄
  3. WHERE id 與 :id 引數相符。請留意 :id。您可以在查詢中使用冒號標記法來參照函式中的引數。
@Query("SELECT * from item WHERE id = :id")
  1. @Query 註解下方,新增 getItem() 函式,這個函式會採用 Int 引數並傳回 Flow<Item>
@Query("SELECT * from item WHERE id = :id")
fun getItem(id: Int): Flow<Item>

使用 FlowLiveData 做為傳回類型,可確保當資料庫中的資料有變更時,您會收到通知。建議您在持續性層中使用 FlowRoom 將隨時為您更新這個 Flow,因此您只需要明確取得資料一次即可。這有助於更新商品目錄清單,您將於下一個程式碼研究室中實作這部分內容。根據 Flow 傳回類型,Room 也會在背景執行緒上執行查詢。您不需要將它明確設為 suspend 函式,並在協同程式範圍內呼叫。

您可能需要從 kotlinx.coroutines.flow.Flow 匯入 Flow

  1. 新增具有 getItems() 函式的 @Query
  2. 讓 SQLite 查詢傳回 item 資料表中的所有資料欄,以遞增順序排序。
  3. getItems()Item 實體清單做為 Flow 傳回。Room 將隨時為您更新這個 Flow,因此您只需要明確取得資料一次即可。
@Query("SELECT * from item ORDER BY name ASC")
fun getItems(): Flow<List<Item>>
  1. 雖然您看不到任何明顯的變更,但請執行應用程式,確定沒有任何錯誤。

7. 建立資料庫例項

在這項工作中,您要建立 RoomDatabase,並使用您在先前工作中建立的 Entity 和 DAO。資料庫類別定義了實體清單和資料存取物件清單。同時也是基礎連線的主要存取點。

Database 類別為您的應用程式提供了定義的 DAO 例項。反過來,應用程式可以使用 DAO 來擷取資料庫中的資料,做為關聯資料實體物件的執行個體。應用程式也可以使用定義的資料實體,更新對應資料表中的資料列,或是建立新的資料列來插入資料。

您需要建立抽象的 RoomDatabase 類別,並加上 @Database 註解。此類別包含一個方法,可在 RoomDatabase 的例項不存在時建立該例項,或者傳回 RoomDatabase 的現有例項。

取得 RoomDatabase 例項的一般程序如下:

  • 建立可擴充 RoomDatabasepublic abstract 類別。您定義的新抽象類別會成為資料庫容器。您定義的類別是抽象的,因為 Room 會為您建立實作。
  • 使用 @Database 為類別加上註解。在引數中,列出資料庫的實體並設定版本號碼。
  • 定義傳回 ItemDao 例項的抽象方法或屬性,Room 會為您產生實作。
  • 整個應用程式只需要一個 RoomDatabase 例項,因此請將 RoomDatabase 設為單例模式。
  • 僅在您的 (item_database) 資料庫不存在的情況下,使用 RoomRoom.databaseBuilder 建立資料庫。否則,請傳回現有資料庫。

建立資料庫

  1. data 套件中,建立 Kotlin 類別 ItemRoomDatabase.kt
  2. ItemRoomDatabase.kt 檔案中,將 ItemRoomDatabase 類別設為可擴充 RoomDatabaseabstract 類別。使用 @Database 為類別加上註解。您將在下一步中修正缺少參數的錯誤。
@Database
abstract class ItemRoomDatabase : RoomDatabase() {}
  1. @Database 註解需要多個引數,以便 Room 能夠建構資料庫。
  • Item 指定為包含 entities 清單的唯一類別。
  • version 設為 1每次變更資料庫資料表的結構定義時,都必須增加版本號碼。
  • 只要將 exportSchema 設為 false,即可不保留結構定義版本記錄的備份。
@Database(entities = [Item::class], version = 1, exportSchema = false)
  1. 資料庫需要知道該 DAO。在類別內文中,宣告一個傳回 ItemDao 的抽象函式。您可以擁有多個 DAO。
abstract fun itemDao(): ItemDao
  1. 在抽象函式下方,定義 companion 物件。夥伴物件可讓您使用類別名稱做為限定詞,建立或取得資料庫。
 companion object {}
  1. companion 物件中,宣告資料庫的私人空值變數 INSTANCE,並將其初始化為 nullINSTANCE 變數會在建立資料庫時保留對該資料庫的參照。這有助於維護在指定時間開啟的資料庫單一例項,該例項是建立及維護成本很高的資源。

使用 @VolatileINSTANCE 加上註解。系統一律不會快取易失變數的值,所有讀取與寫入作業都會在主記憶體中完成。這有助於確保所有執行執行緒的 INSTANCE 值保持在最新狀態且相同。這表示一個執行緒對 INSTANCE 所做的變更將立即對所有其他執行緒可見。

@Volatile
private var INSTANCE: ItemRoomDatabase? = null
  1. INSTANCE 下方的 companion 物件內,使用資料庫建構工具所需的 Context 參數定義 getDatabase() 方法。傳回類型 ItemRoomDatabase。由於 getDatabase() 尚未傳回任何內容,因此系統會顯示錯誤訊息。
fun getDatabase(context: Context): ItemRoomDatabase {}
  1. 多個執行緒可能會產生競爭狀況,並同時要求一個資料庫例項,如此一來就會產生兩個資料庫,而非一個資料庫。納入程式碼以將資料庫納入 synchronized 區塊中,這表示一次只能執行一個執行緒,只有這個執行緒可以進入此程式碼區塊,從而確保系統只會將資料庫初始化一次。

getDatabase() 中,傳回 INSTANCE 變數;如果 INSTANCE 為空值,則在 synchronized{} 區塊內對其進行初始化。使用 elvis 運算子 (?:) 執行此作業。傳入夥伴物件 this,也就是要在函式區塊中鎖定的夥伴物件。您將在後續步驟中修正此錯誤。

return INSTANCE ?: synchronized(this) { }
  1. 在同步區塊中,建立 val 例項變數,並使用資料庫建構工具取得資料庫。您還有錯誤有待在後續步驟中修正。
val instance = Room.databaseBuilder()
  1. synchronized 區塊的結尾,傳回 instance
return instance
  1. synchronized 區塊中,初始化 instance 變數,並使用資料庫建構工具取得資料庫。將應用程式結構定義、資料庫類別以及資料庫的名稱 item_database 傳遞給 Room.databaseBuilder()
val instance = Room.databaseBuilder(
   context.applicationContext,
   ItemRoomDatabase::class.java,
   "item_database"
)

Android Studio 會產生「類型不符」錯誤。如要移除這項錯誤,請按照下列步驟新增遷移策略和 build()

  1. 將必要的遷移策略新增至建構工具。使用 .fallbackToDestructiveMigration()

一般來說,您必須為遷移物件提供有關何時變更結構定義的遷移策略。遷移物件是一種物件,可定義如何擷取舊結構定義中的所有資料列,並將其轉換為新結構定義中的資料列,以免資料遺失。遷移不在本程式碼研究室的範圍內。其中一個簡單的解決方法是刪除並重新建構資料庫,但代表資料會遺失。

.fallbackToDestructiveMigration()
  1. 如要建立資料庫例項,請呼叫 .build()。這應該會移除 Android Studio 錯誤。
.build()
  1. synchronized 區塊內,指派 INSTANCE = instance
INSTANCE = instance
  1. 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
           }
       }
   }
}
  1. 建構程式碼,確保沒有錯誤。

實作應用程式類別

在這項工作中,您需要在應用程式類別中對資料庫例項執行個體化。

  1. 開啟 InventoryApplication.kt,建立類型為 ItemRoomDatabase 且名為 databaseval。在傳入結構定義的 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 即可。

91298a7c05e4f5e0.png

建立商品目錄 ViewModel

  1. com.example.inventory 套件中,建立 Kotlin 類別檔案 InventoryViewModel.kt
  2. ViewModel 類別擴充 InventoryViewModel 類別。將 ItemDao 物件做為參數傳遞至預設建構函式。
class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {}
  1. 在類別外的 InventoryViewModel.kt 檔案結尾,新增 InventoryViewModelFactory 類別,對 InventoryViewModel 執行個體執行個體化。傳入與做為 ItemDao 例項的 InventoryViewModel 相同的建構函式參數。使用 ViewModelProvider.Factory 類別來擴充該類別。您將在下一個步驟中修正未實作方法的錯誤。
class InventoryViewModelFactory(private val itemDao: ItemDao) : ViewModelProvider.Factory {
}
  1. 按一下紅色燈泡並選取「Implement Members」,或是覆寫 ViewModelProvider.Factory 類別中的 create() 方法 (如下所示),這會將任何類別類型當做引數,然後傳回 ViewModel 物件。
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   TODO("Not yet implemented")
}
  1. 實作 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
)

85c644aced4198c5.png

您需要該特定商品的名稱、價格和庫存,以便將實體新增到資料庫。在程式碼研究室的後續部分,您將使用「Add Item」畫面取得使用者的詳細資料。在目前的工作中,您要使用三個字串做為 ViewModel 的輸入內容、將這些字串轉換為 Item 實體例項,並使用 ItemDao 例項將其儲存至資料庫。現在可以開始實作了。

  1. InventoryViewModel 類別中,新增名為 insertItem()private 函式,該函式會擷取 Item 物件,並以非封鎖的方式將資料加入資料庫。
private fun insertItem(item: Item) {
}
  1. 如要透過主執行緒與資料庫互動,請啟動協同程式,然後呼叫其中的 DAO 方法。在 insertItem() 方法中,使用 viewModelScope.launch 來啟動 ViewModelScope 中的協同程式。在啟動函式中,對傳入 itemitemDao 呼叫暫停函式 insert()ViewModelScopeViewModel 類別的擴充功能屬性,會在 ViewModel 刪除時自動取消其子項協同程式。
private fun insertItem(item: Item) {
   viewModelScope.launch {
       itemDao.insert(item)
   }
}

匯入 kotlinx.coroutines.launch, androidx.lifecycle.viewModelScope

com.example.inventory.data.Item (如未自動匯入)。

  1. InventoryViewModel 類別中,新增另一個私人函式,該函式會擷取三個字串,並傳回 Item 例項。
private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item {
   return Item(
       itemName = itemName,
       itemPrice = itemPrice.toDouble(),
       quantityInStock = itemCount.toInt()
   )
}
  1. InventoryViewModel 類別中,新增名為 addNewItem() 的公開函式,該函式會採用三個字串來取得商品詳細資料。將商品詳細資料字串傳遞至 getNewItemEntry() 函式,並將傳回的值指派給名為 newItem 的值。呼叫傳入 newIteminsertItem(),以將新實體新增至資料庫。系統會從使用者介面片段中呼叫,將商品詳細資料新增至資料庫。
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

  1. AddItemFragment.ktAddItemFragment 類別的開頭,建立類型為 InventoryViewModel 且名為 viewModelprivate val。使用 by activityViewModels() Kotlin 屬性委派功能,即可跨片段共用 ViewModel。您將在下一個步驟中修正錯誤。
private val viewModel: InventoryViewModel by activityViewModels {
}
  1. 在 lambda 中,呼叫 InventoryViewModelFactory() 建構函式並傳入 ItemDao 例項。請使用先前其中一項工作中建立的 database 例項,呼叫 itemDao 建構函式。
private val viewModel: InventoryViewModel by activityViewModels {
   InventoryViewModelFactory(
       (activity?.application as InventoryApplication).database
           .itemDao()
   )
}
  1. viewModel 定義下方,建立類型為 Item 且名為 itemlateinit var
 lateinit var item: Item
  1. 「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
}
  1. AddItemFragment.ktonCreateView() 函式下方,建立一個名為 isEntryValid()private 函式,該函式會傳回 Boolean。您將在下一步中修正缺少傳回值這一錯誤。
private fun isEntryValid(): Boolean {
}
  1. 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()
   )
}
  1. AddItemFragment 類別的 isEntryValid() 函式下方,請新增另一個名為 addNewItem()private 函式,該函式沒有參數且不會傳回任何內容。在函式中,在 if 條件內呼叫 isEntryValid()
private fun addNewItem() {
   if (isEntryValid()) {
   }
}
  1. if 區塊內,對 viewModel 例項呼叫 addNewItem() 方法。傳入使用者輸入的商品詳細資料,使用 binding 例項讀取這些資料。
if (isEntryValid()) {
   viewModel.addNewItem(
   binding.itemName.text.toString(),
   binding.itemPrice.text.toString(),
   binding.itemCount.text.toString(),
   )
}
  1. if 區塊下方建立 val action,返回 ItemListFragment。呼叫 findNavController().navigate(),以傳入 action
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)

匯入 androidx.navigation.fragment.findNavController.

  1. 完整方法應如下所示。
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)
       }
}
  1. 如要連結所有內容,請在「Save」按鈕中新增點擊處理常式。AddItemFragment 類別的 onDestroyView() 函式上方,覆寫 onViewCreated() 函式。
  2. onViewCreated() 函式中,將點按處理常式新增至儲存按鈕,並呼叫 addNewItem()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
   binding.saveAction.setOnClickListener {
       addNewItem()
   }
}
  1. 建構並執行應用程式,然後輕觸「+」懸浮動作按鈕 (FAB)。在「Add Item」畫面中,新增商品詳細資料,然後輕觸「Save」。這項操作會儲存資料,但應用程式還不會顯示任何資料。在下一項工作中,您將使用資料庫檢查器瀏覽已儲存的資料。

193c7fa9c41e0819.png

使用資料庫檢查器查看資料庫

  1. 在執行 API 級別 26 或以上的模擬器或已連結的裝置上執行應用程式 (如果您尚未這麼做)。資料庫檢查器最適合在搭載 API 級別 26 的模擬器/裝置上運作。
  2. 在 Android Studio 中,從選單列中選取「View」(檢視畫面) >「Tool Windows」(工具視窗) >「Database Inspector」(資料庫檢查器)。
  3. 在「Database Inspector」窗格中,從下拉式選單中選取 com.example.inventory
  4. 商品目錄應用程式中的「item_database」會顯示在「Databases」窗格中。展開「item_database」的節點,並選取「Item」進行檢查。如果「Databases」窗格沒有任何內容,請使用模擬器的「Add Item」畫面,將某些商品新增至資料庫。
  5. 勾選資料庫檢查器中的「Live updates」核取方塊,在您與模擬器或裝置上執行的應用程式互動時,自動更新系統顯示的資料。

4803c08f94e34118.png

恭喜!您已經建立了一個應用程式,可使用 Room 保存資料。在接下來的程式碼研究室中,您將在應用程式中新增 RecyclerView 以顯示資料庫中的商品,並在應用程式中新增刪除及更新實體等功能。到時見!

10. 解決方案程式碼

本程式碼研究室的解決方案程式碼位於以下所示的 GitHub 存放區和分支版本中。

如要取得這個程式碼研究室的程式碼,並在 Android Studio 中開啟,請按照下列步驟操作:

取得程式碼

  1. 按一下所提供的網址。系統會在瀏覽器中開啟專案的 GitHub 頁面。
  2. 在專案的 GitHub 頁面中,按一下「Code」按鈕開啟對話方塊。

5b0a76c50478a73f.png

  1. 在對話方塊中,按一下「Download ZIP」按鈕,將專案儲存到電腦。等待下載作業完成。
  2. 在電腦上尋找該檔案 (可能位於「下載」資料夾中)。
  3. 按兩下 ZIP 檔案,將檔案解壓縮。這項操作會建立含有專案檔案的新資料夾。

在 Android Studio 中開啟專案

  1. 啟動 Android Studio。
  2. 在「Welcome to Android Studio」視窗中,按一下「Open an existing Android Studio project」

36cc44fcf0f89a1d.png

注意:如果 Android Studio 已開啟,請依序選取「File」>「New」>「Import Project」選單選項。

21f3eec988dcfbe9.png

  1. 在「Import Project」對話方塊中,前往解壓縮專案資料夾所在的位置 (可能位於「下載」資料夾中)。
  2. 按兩下該專案資料夾。
  3. 等待 Android Studio 開啟專案。
  4. 按一下「Run」按鈕 11c34fc5e516fb1c.png 即可建構並執行應用程式。請確認應用程式的建構符合預期。
  5. 在「Project」工具視窗中瀏覽專案檔案,查看應用程式的設定方式。

11. 摘要

  • 將資料表定義為具有 @Entity 註解的資料類別。定義具有 @ColumnInfo 註解的屬性做為資料表中的資料欄。
  • 定義資料存取物件 (DAO) 做為標有 @Dao 註解的介面。DAO 會將 Kotlin 函式對應至資料庫查詢。
  • 使用註解來定義 @Insert@Delete@Update 函式。
  • 針對其他查詢,使用 @Query 註解搭配 SQLite 查詢字串做為參數。
  • 使用資料庫檢查器查看儲存在 Android SQLite 資料庫中的資料。

12. 瞭解詳情

Android 開發人員說明文件

網誌文章

影片

其他說明文件和文章