1. 事前準備
在先前的程式碼研究室中,您學到如何使用 Room 持續性程式庫儲存應用程式資料,這個程式庫是 SQLite 資料庫頂端的抽象層。在本程式碼研究室中,您要為商品目錄應用程式新增更多功能,並學習如何使用 Room 讀取、顯示、更新及刪除 SQLite 資料庫中的資料。您將使用 RecyclerView
顯示資料庫中的資料,並在資料庫中的基礎資料發生變更時自動更新資料。
必要條件
- 您瞭解如何使用 Room 程式庫建立 SQLite 資料庫並與之互動。
- 您瞭解如何建立實體、DAO 和資料庫類別。
- 您瞭解如何使用資料存取物件 (DAO) 將 Kotlin 函式對應至 SQL 查詢。
- 您瞭解如何在
RecyclerView
中顯示清單項目。 - 您參加了本單元的前一個程式碼研究室,即使用 Room 持續保存資料
課程內容
- 如何讀取及顯示 SQLite 資料庫中的實體。
- 如何使用 Room 程式庫更新和刪除 SQLite 資料庫中的實體。
建構項目
- 您要建構一個商品目錄應用程式,用於顯示商品目錄的商品清單。此應用程式可使用 Room 更新、編輯及刪除應用程式資料庫中的項目。
2. 範例應用程式總覽
本程式碼研究室會使用先前程式碼研究室的 Inventory 應用程式解決方案程式碼,做為範例程式碼。範例應用程式已使用 Room 持續性程式庫儲存資料。使用者可以透過「Add Item」畫面,將資料新增至應用程式資料庫。
注意:目前版本的範例應用程式不會顯示資料庫中儲存的日期。
在本程式碼研究室中,您將擴充應用程式,使其使用 Room 程式庫讀取及顯示資料庫中的資料,以及更新及刪除資料庫中的實體。
下載這個程式碼研究室的範例程式碼
這個範例程式碼與先前程式碼研究室的解決方案程式碼相同。
如要取得本程式碼研究室的程式碼,並在 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」工具視窗中瀏覽專案檔案,瞭解應用程式的實作方式。
3. 新增 RecyclerView
在這項工作中,您要在應用程式中新增 RecyclerView
,以顯示資料庫中儲存的資料。
新增輔助函式來設定價格格式
以下是最終應用程式的螢幕截圖。
請注意,價格會以貨幣格式顯示。如要將雙值換算成所需貨幣格式,請在 Item
類別中加入擴充功能函式。
擴充功能函式
Kotlin 能夠使用新功能擴充類別,無需沿用類別或修改類別的現有定義。這表示您可以將函式加入現有類別,而無需存取其原始碼。方法是透過名為擴充功能的特殊宣告來執行。
舉例來說,您可以在無法修改的第三方程式庫中,為某個類別編寫新的函式。這類函式可以照常呼叫,就像是原始類別的方法一樣。這些函式稱為擴充功能函式。(您也可以使用擴充功能屬性為現有類別定義新屬性,不過這部分內容不在本程式碼研究室的介紹範圍內)。
擴充功能函式實際上不會修改類別,但可以讓您在呼叫該類別物件的函式時使用點號標記法。
例如,在下列程式碼片段中,有一個名為 Square
的類別。此類別內會有正方形四邊的屬性,以及計算正方形面積的函式。請注意,Square.perimeter()
擴充功能函式的函式名稱開頭是其使用的類別。在函式中,您可以參照 Square
類別的公開屬性。
查看 main()
函式中的擴充功能函式使用方式。建立的擴充功能函式 perimeter()
會在該 Square
類別中做為一般函式呼叫。
範例:
class Square(val side: Double){
fun area(): Double{
return side * side;
}
}
// Extension function to calculate the perimeter of the square
fun Square.perimeter(): Double{
return 4 * side;
}
// Usage
fun main(args: Array<String>){
val square = Square(5.5);
val perimeterValue = square.perimeter()
println("Perimeter: $perimeterValue")
val areaValue = square.area()
println("Area: $areaValue")
}
在這個步驟中,您要將商品價格轉換為貨幣格式字串。一般而言,建議您不要只是為了設定資料格式而去變更代表資料的實體類別 (請參見單一責任原則),您可改為新增擴充功能函式。
- 在
Item.kt
的類別定義下方,新增名為Item.getFormattedPrice()
的擴充功能函式,該函式不使用參數並且會傳回String
。請注意函式名稱中的類別名稱和點號標記。
fun Item.getFormattedPrice(): String =
NumberFormat.getCurrencyInstance().format(itemPrice)
請在 Android Studio 出現提示時匯入 java.text.NumberFormat
。
新增 ListAdapter
在這個步驟中,您要在 RecyclerView
中新增清單轉接器。鑒於您熟悉了之前程式碼研究室中有關實作轉接器的內容,操作說明匯總如下。為了方便起見,這個步驟最後提供了完整的 ItemListAdapter
檔案,可協助您瞭解程式碼研究室中的 Room 概念。
- 在
com.example.inventory
套件中,新增名為ItemListAdapter
的 Kotlin 類別。傳入名為onItemClicked()
的函式做為建構函式參數,並將Item
物件做為參數使用。 - 變更
ItemListAdapter
類別簽名來擴充ListAdapter
。傳入Item
和ItemListAdapter.ItemViewHolder
做為參數。 - 新增建構函式參數
DiffCallback
;ListAdapter
會使用這項參數來偵測清單中的變更內容。 - 覆寫必要的方法
onCreateViewHolder()
和onBindViewHolder()
。 onCreateViewHolder()
方法會在 RecyclerView 需要時傳回新的ViewHolder
。- 在
onCreateViewHolder()
方法中,建立一個新的View
,並使用自動產生的繫結類別ItemListItemBinding
從item_list_item.xml
版面配置檔案加載它。 - 實作
onBindViewHolder()
方法。使用getItem()
方法取得目前商品,並傳遞位置。 - 在
itemView
上設定點按事件監聽器,在事件監聽器中呼叫函式onItemClicked()
。 - 定義
ItemViewHolder
類別,並從RecyclerView.ViewHolder.
擴充該類別。覆寫bind()
函式,並傳入Item
物件。 - 定義夥伴物件。在伴生物件中,定義類型為
DiffUtil.ItemCallback<Item>()
且名稱為DiffCallback
的val
。覆寫必要的方法areItemsTheSame()
和areContentsTheSame()
,然後加以定義。
完成的類別應如下所示:
package com.example.inventory
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.inventory.data.Item
import com.example.inventory.data.getFormattedPrice
import com.example.inventory.databinding.ItemListItemBinding
/**
* [ListAdapter] implementation for the recyclerview.
*/
class ItemListAdapter(private val onItemClicked: (Item) -> Unit) :
ListAdapter<Item, ItemListAdapter.ItemViewHolder>(DiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
return ItemViewHolder(
ItemListItemBinding.inflate(
LayoutInflater.from(
parent.context
)
)
)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val current = getItem(position)
holder.itemView.setOnClickListener {
onItemClicked(current)
}
holder.bind(current)
}
class ItemViewHolder(private var binding: ItemListItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Item) {
}
}
companion object {
private val DiffCallback = object : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem.itemName == newItem.itemName
}
}
}
}
從完成的應用程式 (本程式碼研究室結尾處的解決方案應用程式) 查看商品目錄清單畫面。請注意,每個清單元素都會顯示商品目錄商品的名稱、以貨幣格式顯示的價格,以及目前的庫存。在先前的步驟中,您使用了 item_list_item.xml
版面配置檔案和三個 TextView 來建立資料列。在下一個步驟中,您要將實體的詳細資料繫結至這些 TextView。
- 在
ItemListAdapter.kt
的ItemViewHolder
類別中實作bind()
函式。將itemName
TextView 繫結至item.itemName
。使用getFormattedPrice()
擴充功能函式以貨幣格式取得價格,並將其繫結至itemPrice
TextView。將quantityInStock
值轉換為String
,並將其繫結至itemQuantity
TextView。已完成的方法應如下所示:
fun bind(item: Item) {
binding.apply {
itemName.text = item.itemName
itemPrice.text = item.getFormattedPrice()
itemQuantity.text = item.quantityInStock.toString()
}
}
在 Android Studio 顯示提示時,匯入 com.example.inventory.data.getFormattedPrice
。
使用 ListAdapter
在這項工作中,您要更新 InventoryViewModel
和 ItemListFragment
,以便使用您在上一個步驟中建立的清單轉接程式,在畫面中顯示商品詳細資料。
- 在
InventoryViewModel
類別的開頭,針對資料庫中的商品,建立一個名為allItems
且類型為LiveData<List<Item>>
的val
。如果出現錯誤,別擔心,您將很快進行修正。
val allItems: LiveData<List<Item>>
請在 Android Studio 出現提示時匯入 androidx.lifecycle.LiveData
。
- 在
itemDao
上呼叫getItems()
,並指派給allItems
。getItems()
函式會傳回Flow
。如要以LiveData
值的形式使用資料,請使用asLiveData()
函式。已完成的定義應如下所示:
val allItems: LiveData<List<Item>> = itemDao.getItems().asLiveData()
請在 Android Studio 出現提示時匯入 androidx.lifecycle.asLiveData
。
- 在
ItemListFragment
中,請於類別開頭宣告一個類型為InventoryViewModel
的private
不可變屬性viewModel
。使用by
委派,將屬性初始化作業傳送至activityViewModels
類別。傳入InventoryViewModelFactory
建構函式。
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database.itemDao()
)
}
收到 Android Studio 要求時匯入 androidx.fragment.app.activityViewModels
。
- 同樣在
ItemListFragment
中,捲動至函式onViewCreated()
。在super.onViewCreated()
呼叫下方,宣告名為adapter
的val
。使用預設建構函式初始化新的adapter
屬性,ItemListAdapter{}
不會傳入任何內容。 - 將新建立的
adapter
繫結至recyclerView
,如下所示:
val adapter = ItemListAdapter {
}
binding.recyclerView.adapter = adapter
- 設定轉接器後,仍位於
onViewCreated()
內。在allItems
上附加觀察器,監聽資料變更。 - 在觀察器內,對
adapter
呼叫submitList()
,並傳遞新的清單。此操作會使用清單中的新商品更新 RecyclerView。
viewModel.allItems.observe(this.viewLifecycleOwner) { items ->
items.let {
adapter.submitList(it)
}
}
- 確認已完成的
onViewCreated()
方法如下所示。執行應用程式。請注意,如果您將商品儲存在應用程式資料庫中,則系統會顯示商品目錄清單。如果清單為空白,請新增部分商品目錄商品至應用程式資料庫。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = ItemListAdapter {
}
binding.recyclerView.adapter = adapter
viewModel.allItems.observe(this.viewLifecycleOwner) { items ->
items.let {
adapter.submitList(it)
}
}
binding.recyclerView.layoutManager = LinearLayoutManager(this.context)
binding.floatingActionButton.setOnClickListener {
val action = ItemListFragmentDirections.actionItemListFragmentToAddItemFragment(
getString(R.string.add_fragment_title)
)
this.findNavController().navigate(action)
}
}
4. 顯示商品詳細資料
在這項工作中,您將在「Item Details」畫面中讀取並顯示實體詳細資料。您要使用主鍵 (也就是商品 id
) 來從商品目錄應用程式資料庫中讀取詳細資料 (例如名稱、價格和數量),並使用 fragment_item_detail.xml
版面配置檔案將這些詳細資料顯示在「Item Details」畫面上。我們已預先為您設計好版面配置檔案 (fragment_item_detail.xml
),當中包含三個用來顯示商品詳細資料的 TextView。
在這項工作中,您將執行下列步驟:
- 在 RecyclerView 中新增點按處理常式,將應用程式移至「Item Details」畫面。
- 在
ItemListFragment
片段中,從資料庫擷取並顯示資料。 - 將 TextView 繫結至 ViewModel 資料。
新增點按處理常式
- 在
ItemListFragment
中,捲動至onViewCreated()
函式,即可更新轉接器定義。 - 將 lambda 新增為
ItemListAdapter{}
的建構函式參數。
val adapter = ItemListAdapter {
}
- 在 lambda 中,建立名為
action
的val
。您很快便要修正初始化錯誤。
val adapter = ItemListAdapter {
val action
}
- 對
ItemListFragmentDirections
呼叫actionItemListFragmentToItemDetailFragment()
方法,傳入商品id
。將傳回的NavDirections
物件指派給action
。
val adapter = ItemListAdapter {
val action = ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
}
- 在
action
定義下方,使用this.
findNavController
()
擷取NavController
執行個體,然後對其呼叫navigate()
,傳入action
。轉接器的定義如下所示:
val adapter = ItemListAdapter {
val action = ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
this.findNavController().navigate(action)
}
- 執行應用程式。在
RecyclerView
中按一下某個商品。應用程式將轉至「Item Details」畫面。請注意,詳細資料為空白。輕觸按鈕,系統沒有回應。
在接下來的步驟中,您要在「Item Details」畫面中顯示實體詳細資料,並為「Sell」和「Delete」按鈕新增功能。
擷取商品詳細資料
在這個步驟中,您將在 InventoryViewModel
中新增函式,以根據商品 id
從資料庫中擷取商品詳細資料。在下一個步驟中,您將使用此函式在「Item Details」畫面中顯示實體詳細資料。
- 在
InventoryViewModel
中,新增名為retrieveItem()
的函式,該函式會以Int
做為項目 ID,並傳回LiveData<Item>
。您很快便要修正傳回運算式錯誤。
fun retrieveItem(id: Int): LiveData<Item> {
}
- 在新函式中,對
itemDao
呼叫getItem()
,並傳入參數id
。getItem()
函式會傳回Flow
。如要將Flow
值做為LiveData
使用,請呼叫asLiveData()
函式,並將此函式做為retrieveItem()
函式的傳回。已完成的函式應如下所示:
fun retrieveItem(id: Int): LiveData<Item> {
return itemDao.getItem(id).asLiveData()
}
將資料繫結至 TextView
在這個步驟中,您將在 ItemDetailFragment
中建立 ViewModel 例項,並將 ViewModel 資料繫結至「Item Details」畫面中的 TextView。您還要將觀察器附加至 ViewModel 中的資料,以便在資料庫中的基礎資料有所變更時,隨時更新畫面上的商品目錄清單。
- 在
ItemDetailFragment
中,新增類型Item
實體的item
可變動屬性。這個屬性可用來儲存單一實體的相關資訊。這個屬性稍後會初始化,請在前面加上lateinit
。
lateinit var item: Item
請在 Android Studio 出現提示時匯入 com.example.inventory.data.Item
。
- 請於類別
ItemDetailFragment
開頭宣告一個類型為InventoryViewModel
的private
不可變屬性viewModel
。使用by
委派,將屬性初始化作業傳送至activityViewModels
類別。傳入InventoryViewModelFactory
建構函式。
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database.itemDao()
)
}
如果 Android Studio 出現提示,請匯入 androidx.fragment.app.activityViewModels
。
- 同樣在
ItemDetailFragment
中,建立名為bind()
的private
函式,該函式會採用Item
實體的執行個體做為參數,並且不會傳回任何內容。
private fun bind(item: Item) {
}
- 實作
bind()
函式,這與您在ItemListAdapter
中所進行的操作類似。將itemName
TextView 的text
屬性設為item.itemName
。對item
屬性呼叫getFormattedPrice
()
,以設定價格值的格式,然後將其設為itemPrice
TextView 的text
屬性。將quantityInStock
轉換為String
,並將其設為itemQuantity
TextView 的text
屬性。
private fun bind(item: Item) {
binding.itemName.text = item.itemName
binding.itemPrice.text = item.getFormattedPrice()
binding.itemCount.text = item.quantityInStock.toString()
}
- 更新
bind()
函式,以對程式碼區塊使用apply{}
範圍函式,如下所示。
private fun bind(item: Item) {
binding.apply {
itemName.text = item.itemName
itemPrice.text = item.getFormattedPrice()
itemCount.text = item.quantityInStock.toString()
}
}
- 還是在
ItemDetailFragment
中,覆寫onViewCreated()
。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
- 在之前某個步驟中,您已從
ItemListFragment
將商品 ID 做為導覽引數傳遞至ItemDetailFragment
在onViewCreated()
中的超類別函式呼叫下方,建立名為id
的不可變變數。擷取瀏覽引數並將其指派給這個新的變數。
val id = navigationArgs.itemId
- 接著,請使用這個
id
變數來擷取商品詳細資料。同樣在onViewCreated()
中,對傳入id
的viewModel
呼叫retrieveItem()
函式。將觀察器附加至傳入viewLifecycleOwner
和 lambda 的傳回值。
viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) {
}
- 在 lambda 中,傳入
selectedItem
做為參數,其中包含擷取自資料庫的Item
實體。在 lambda 函式內文中,將selectedItem
值指派給item
。呼叫傳入item
的bind()
函式。已完成的函式應如下所示。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val id = navigationArgs.itemId
viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) { selectedItem ->
item = selectedItem
bind(item)
}
}
- 執行應用程式。按一下「Inventory」畫面上的任何清單元素,系統會隨即顯示「Item Details」畫面。請注意,現在畫面不再為空白,而是顯示擷取自商品目錄資料庫的實體詳細資料。
- 依序輕觸「Sell」按鈕、「Delete」按鈕和懸浮動作按鈕 (FAB)。沒有回應!接下來的工作中,您將實作這些按鈕的功能。
5. 實作銷售商品
在這項工作中,您要擴充應用程式功能,並實作銷售功能。以下是這個步驟的詳細操作說明。
- 在 ViewModel 中新增函式以更新實體
- 建立一個新方法,用於減少數量,同時更新應用程式資料庫中的實體。
- 將點按事件監聽器附加到「Sell」按鈕
- 如果數量為零,請停用「Sell」按鈕。
編寫程式碼:
- 在
InventoryViewModel
中,新增名為updateItem()
的私人函式,該函式會使用實體類別Item
的執行個體且不會傳回任何內容。
private fun updateItem(item: Item) {
}
- 實作新方法:
updateItem()
。如要從ItemDao
類別呼叫update()
停權方法,請使用viewModelScope
啟動協同程式。在啟動區塊中,對傳入item
的itemDao
呼叫update()
函式。已完成的方法應如下所示。
private fun updateItem(item: Item) {
viewModelScope.launch {
itemDao.update(item)
}
}
- 依舊在
InventoryViewModel
中,新增其他名為sellItem()
的方法,該方法會使用Item
實體類別的執行個體,且不會傳回任何內容。
fun sellItem(item: Item) {
}
- 在
sellItem()
函式中,新增if
條件來檢查item.quantityInStock
是否大於0
。
fun sellItem(item: Item) {
if (item.quantityInStock > 0) {
}
}
在 if
區塊內,您需要使用資料類別的 copy()
函式來更新實體。
資料類別:copy()
根據預設,系統會向資料類別的所有執行個體提供 copy()
函式。此函式可用於複製物件來變更其部分屬性,但保留其餘屬性。
舉例來說,考慮如下所示的 User
類別及其例項 jack
。如要建立新的執行個體且僅更新 age
屬性,則實作方法如下:
範例
// Data class
data class User(val name: String = "", val age: Int = 0)
// Data class instance
val jack = User(name = "Jack", age = 1)
// A new instance is created with its age property changed, rest of the properties unchanged.
val olderJack = jack.copy(age = 2)
- 返回
InventoryViewModel
中的sellItem()
函式。在if
區塊中,建立名為newItem
的新不可變屬性。在傳入更新的quantityInStock
的item
例項上呼叫copy()
函式,藉此減少1
庫存。
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
- 在
newItem
定義下方,呼叫傳入新的更新實體 (即newItem
) 的updateItem()
函式。已完成的方法應如下所示。
fun sellItem(item: Item) {
if (item.quantityInStock > 0) {
// Decrease the quantity by 1
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
updateItem(newItem)
}
}
- 如要新增銷售庫存功能,請前往
ItemDetailFragment
。捲動至bind()
函式的結尾。在apply
區塊內,為「Sell」按鈕設定點擊事件監聽器,並在viewModel
上呼叫sellItem()
函式。
private fun bind(item: Item) {
binding.apply {
...
sellItem.setOnClickListener { viewModel.sellItem(item) }
}
}
- 執行應用程式。在「Inventory」畫面中,按一下數量大於零的清單元素。系統隨即顯示「Item Details」畫面。輕觸「Sell」按鈕。請注意數量值會減少一個單位。
- 在「Item Details」畫面上,連續輕觸「Sell」按鈕即可將數量設為 0 (提示:選取存貨較少的實體,或建立數量較少的新實體)。數量設為 0 後,輕觸「Sell」按鈕。不會出現明顯的變更。這是因為函式
sellItem()
會先檢查數量是否大於零,然後再更新數量。
- 如要為使用者提供更符合需求的意見回饋,建議您在沒有商品可銷售時停用「Sell」(銷售) 按鈕。在
InventoryViewModel
中,新增函式,檢查數量是否大於0
。將函式命名為isStockAvailable()
,該函式使用Item
例項並傳回Boolean
。
fun isStockAvailable(item: Item): Boolean {
return (item.quantityInStock > 0)
}
- 前往
ItemDetailFragment
,捲動至bind()
函式。在套用區塊中,在傳入item
的viewModel
上呼叫isStockAvailable()
函式。請將傳回的值設為「Sell」(銷售) 按鈕的isEnabled
屬性。程式碼應如下所示。
private fun bind(item: Item) {
binding.apply {
...
sellItem.isEnabled = viewModel.isStockAvailable(item)
sellItem.setOnClickListener { viewModel.sellItem(item) }
}
}
- 執行應用程式,請注意,當庫存數量為 0 時,系統將停用「Sell」按鈕。恭喜您在應用程式中實作了銷售商品功能。
刪除商品實體
與上一項工作一樣,您將實作刪除功能,進一步擴充應用程式的功能。以下是這個步驟的詳細操作說明,相比於實作銷售功能更加輕鬆。
- 在 ViewModel 中新增函式,以從資料庫中刪除實體
- 在
ItemDetailFragment
中新增方法,以呼叫新的刪除函式並處理導覽。 - 將點擊事件監聽器附加到「Delete」按鈕。
繼續編寫程式碼:
- 在
InventoryViewModel
中,新增名為deleteItem()
的函式,該函式會使用名為item
的Item
實體類別的執行個體,但不會傳回任何內容。在deleteItem()
函式中,使用viewModelScope
啟動協同程式。在launch
區塊內,對傳入item
的itemDao
呼叫delete()
方法。
fun deleteItem(item: Item) {
viewModelScope.launch {
itemDao.delete(item)
}
}
- 在
ItemDetailFragment
中,捲動至deleteItem()
函式的開頭。對viewModel
呼叫deleteItem()
,並傳入item
。item
例項會包含目前顯示在「Item Details」畫面上的實體。已完成的方法應如下所示。
private fun deleteItem() {
viewModel.deleteItem(item)
findNavController().navigateUp()
}
- 還是在
ItemDetailFragment
中,捲動至showConfirmationDialog()
函式。這個函式會在範例程式碼中提供給您。這個方法會顯示快訊對話方塊,以便在刪除商品前取得使用者的確認,並在使用者輕觸肯定按鈕時呼叫deleteItem()
函式。
private fun showConfirmationDialog() {
MaterialAlertDialogBuilder(requireContext())
...
.setPositiveButton(getString(R.string.yes)) { _, _ ->
deleteItem()
}
.show()
}
showConfirmationDialog()
函式會顯示快訊對話方塊,如下所示:
- 在
ItemDetailFragment
中,請在bind()
函式結尾的apply
區塊內,將設定 Delete 按鈕的點擊事件監聽器。呼叫點按事件監聽器 lambda 中的showConfirmationDialog()
。
private fun bind(item: Item) {
binding.apply {
...
deleteItem.setOnClickListener { showConfirmationDialog() }
}
}
- 執行應用程式!在「Inventory」清單畫面中選取清單元素,然後在「Item Details」畫面中輕觸「Delete」按鈕。輕觸「Yes」,應用程式將返回「Inventory」畫面。請注意,已刪除的實體已不在應用程式資料庫中。恭喜您實作了刪除功能。
編輯商品實體
與先前的工作類似,在這項工作中,您要在應用程式中新增另一項增強功能。您將實作編輯商品實體。
以下為編輯應用程式資料庫中實體的快速步驟:
- 將片段標題更新為「編輯商品」,藉此重複使用「Add Item」(新增商品) 畫面。
- 將點擊事件監聽器新增至懸浮動作按鈕 (FAB),前往「Edit Item」畫面。
- 為 TextView 填入實體詳細資料。
- 使用 Room 更新資料庫中的實體。
將點按事件監聽器新增至懸浮動作按鈕 (FAB)
- 在
ItemDetailFragment
中,新增名為editItem()
的private
函式,該函式不採用任何參數,且不會傳回任何內容。在下一個步驟中,請將畫面標題更新為「Edit Item」,這樣就能重複使用fragment_add_item.xml
。為實作此操作,您需要傳送片段標題字串以及商品 ID。
private fun editItem() {
}
更新片段標題後,「Edit Item」畫面應顯示如下。
- 在
editItem()
函式中,建立名為action
的不可變變數。對傳入標題字串edit_fragment_title
和商品id
的ItemDetailFragmentDirections
呼叫actionItemDetailFragmentToAddItemFragment()
。將傳回的值指派給action
。在action
定義下方,呼叫this.findNavController().navigate()
並傳入action
,前往「Edit Item」畫面。
private fun editItem() {
val action = ItemDetailFragmentDirections.actionItemDetailFragmentToAddItemFragment(
getString(R.string.edit_fragment_title),
item.id
)
this.findNavController().navigate(action)
}
- 還是在
ItemDetailFragment
中,捲動至bind()
函式。在apply
區塊內,為懸浮動作按鈕 (FAB) 設定點擊事件監聽器,以及從 lambda 呼叫editItem()
函式,以便前往「Edit Item」畫面。
private fun bind(item: Item) {
binding.apply {
...
editItem.setOnClickListener { editItem() }
}
}
- 執行應用程式。前往「Item Details」畫面。按一下懸浮動作按鈕 (FAB)。請注意,螢幕標題已更新為「編輯商品」,但所有文字欄位均為空白。在下一個步驟中,您將修正這個問題。
填入 TextView
在這個步驟中,您要在「Edit Item」螢幕的文字欄位中填入實體詳細資料。由於我們使用的是 Add Item
螢幕,因此您要在 Kotlin 檔案 AddItemFragment.kt
中新增函式。
- 在
AddItemFragment
中,新增private
函式,將文字欄位與實體詳細資料繫結在一起。為函式bind()
命名,該函式會使用商品實體類別的例項,且不會傳回任何內容。
private fun bind(item: Item) {
}
bind()
函式的實作方式與先前在ItemDetailFragment
中的實作類似。在bind()
函式中,使用format()
函式將價格四捨五入至小數點後兩位,並指派給名為price
的val
,如下所示。
val price = "%.2f".format(item.itemPrice)
- 在
price
定義下方,針對binding
屬性使用apply
範圍函式,如下所示。
binding.apply {
}
- 在
apply
範圍函式程式碼區塊內,將item.itemName
設為itemName
的文字屬性。使用setText()
函式,並傳入item.itemName
字串和TextView.BufferType.SPANNABLE
做為BufferType
。
binding.apply {
itemName.setText(item.itemName, TextView.BufferType.SPANNABLE)
}
如果 Android Studio 出現提示,請匯入 android.widget.TextView
。
- 類似於上述步驟,設定價格
EditText
的文字屬性,如下所示。如要設定 EditEdit 數量的text
屬性,請記得將item.quantityInStock
轉換為String
。您完成的函式應如下所示。
private fun bind(item: Item) {
val price = "%.2f".format(item.itemPrice)
binding.apply {
itemName.setText(item.itemName, TextView.BufferType.SPANNABLE)
itemPrice.setText(price, TextView.BufferType.SPANNABLE)
itemCount.setText(item.quantityInStock.toString(), TextView.BufferType.SPANNABLE)
}
}
- 在
AddItemFragment
中,捲動至onViewCreated()
函式。在呼叫超級類別函式之後。建立名為id
的val
,並從導覽引數擷取itemId
。
val id = navigationArgs.itemId
- 新增帶有條件的
if-else
區塊,以檢查id
是否大於零,並將「Save」按鈕的點擊事件監聽器移至else
區塊。在if
區塊內,使用id
擷取實體,然後在其中新增觀察器。在觀察器內部,更新item
屬性並呼叫傳入item
的bind()
。系統會提供完整函式供您複製貼上。這個函式簡單易懂,您可以自行研究。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val id = navigationArgs.itemId
if (id > 0) {
viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) { selectedItem ->
item = selectedItem
bind(item)
}
} else {
binding.saveAction.setOnClickListener {
addNewItem()
}
}
}
- 執行應用程式,前往「Item Details」,然後輕觸「+」這個懸浮動作按鈕 (FAB)。請注意,這些欄位會填入商品詳細資料。編輯庫存數量或任何其他欄位,然後輕觸「Save」按鈕。沒有回應!這是因為您並未更新應用程式資料庫中的實體。您很快就會解決這個問題。
使用 Room 更新實體
在最後一項工作中,新增最後一段程式碼來實作更新功能。您要在 ViewModel 中定義必要的函式,並在 AddItemFragment
中使用它們。
又要編寫程式碼了。
- 在
InventoryViewModel
中,新增名為getUpdatedItemEntry()
的private
函式,該函式使用Int
,以及實體詳細資料三個名為itemName
、itemPrice
和itemCount
的字串。傳回函式的Item
例項。程式碼僅供參考。
private fun getUpdatedItemEntry(
itemId: Int,
itemName: String,
itemPrice: String,
itemCount: String
): Item {
}
- 在
getUpdatedItemEntry()
函式中,使用函式參數建立商品例項,如下所示。傳回函式的Item
例項。
private fun getUpdatedItemEntry(
itemId: Int,
itemName: String,
itemPrice: String,
itemCount: String
): Item {
return Item(
id = itemId,
itemName = itemName,
itemPrice = itemPrice.toDouble(),
quantityInStock = itemCount.toInt()
)
}
- 同樣在
InventoryViewModel
中,新增其他名為updateItem()
的函式。此函式也會使用Int
和實體詳細資料的三個字串,且不會傳回任何內容。使用以下程式碼片段中的變數名稱。
fun updateItem(
itemId: Int,
itemName: String,
itemPrice: String,
itemCount: String
) {
}
- 在
updateItem()
函式中,呼叫傳入實體資訊的getUpdatedItemEntry()
函式,該函式會以函式參數的形式傳遞,如下所示。將傳回的值指派給名為updatedItem
的不可變變數。
val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
- 在
getUpdatedItemEntry()
函式呼叫的正下方,呼叫傳入updatedItem
的updateItem()
函式。已完成的函式如下所示:
fun updateItem(
itemId: Int,
itemName: String,
itemPrice: String,
itemCount: String
) {
val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
updateItem(updatedItem)
}
- 返回
AddItemFragment
,新增名為updateItem()
的私人函式,該函式不含任何參數,且不會傳回任何內容。在函式中,新增if
條件,以透過呼叫函式isEntryValid()
來驗證使用者輸入內容。
private fun updateItem() {
if (isEntryValid()) {
}
}
- 在
if
區塊中,呼叫傳遞實體詳細資料的viewModel.updateItem()
。請使用導覽引數中的itemId
,以及 EditText 中的其他實體詳細資料,例如名稱、價格和數量,如下所示。
viewModel.updateItem(
this.navigationArgs.itemId,
this.binding.itemName.text.toString(),
this.binding.itemPrice.text.toString(),
this.binding.itemCount.text.toString()
)
- 在
updateItem()
函式呼叫下方,定義名為action
的val
。對AddItemFragmentDirections
呼叫actionAddItemFragmentToItemListFragment()
,並將傳回的值指派給action
。請瀏覽至ItemListFragment
,並呼叫傳入action
的findNavController().navigate()
。
private fun updateItem() {
if (isEntryValid()) {
viewModel.updateItem(
this.navigationArgs.itemId,
this.binding.itemName.text.toString(),
this.binding.itemPrice.text.toString(),
this.binding.itemCount.text.toString()
)
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)
}
}
- 還是在
AddItemFragment
中,捲動至bind()
函式。在binding.
apply
範圍函式區塊中,為「Save」按鈕設定點按事件監聽器。呼叫 lambda 中的updateItem()
函式,如下所示。
private fun bind(item: Item) {
...
binding.apply {
...
saveAction.setOnClickListener { updateItem() }
}
}
- 執行應用程式!嘗試編輯商品目錄商品;您應該可以編輯商品目錄應用程式資料庫中的任何商品。
恭喜您成功建立第一款應用程式,能夠使用 Room 管理應用程式的資料庫!
6. 解決方案程式碼
本程式碼研究室的解決方案程式碼位於以下所示的 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」工具視窗中瀏覽專案檔案,瞭解應用程式的實作方式。
7. 摘要
- Kotlin 能夠使用新功能擴充類別,無需沿用類別或修改類別的現有定義。方法是透過名為擴充功能的特殊宣告來執行。
- 如要以
LiveData
值的形式使用Flow
資料,請使用asLiveData()
函式。 - 根據預設,系統會向資料類別的所有執行個體提供
copy()
函式。這樣您便能複製物件並變更其部分屬性,而保留其餘的屬性。
8. 瞭解詳情
Android 開發人員說明文件
API 參照
Kotlin 參考資料