透過 Room 讀取及更新資料

1. 事前準備

在先前的程式碼研究室中,您學到如何使用 Room 持續性程式庫儲存應用程式資料,這個程式庫是 SQLite 資料庫頂端的抽象層。在本程式碼研究室中,您要為商品目錄應用程式新增更多功能,並學習如何使用 Room 讀取、顯示、更新及刪除 SQLite 資料庫中的資料。您將使用 RecyclerView 顯示資料庫中的資料,並在資料庫中的基礎資料發生變更時自動更新資料。

必要條件

  • 您瞭解如何使用 Room 程式庫建立 SQLite 資料庫並與之互動。
  • 您瞭解如何建立實體、DAO 和資料庫類別。
  • 您瞭解如何使用資料存取物件 (DAO) 將 Kotlin 函式對應至 SQL 查詢。
  • 您瞭解如何在 RecyclerView 中顯示清單項目。
  • 您參加了本單元的前一個程式碼研究室,即使用 Room 持續保存資料

課程內容

  • 如何讀取及顯示 SQLite 資料庫中的實體。
  • 如何使用 Room 程式庫更新和刪除 SQLite 資料庫中的實體。

建構項目

  • 您要建構一個商品目錄應用程式,用於顯示商品目錄的商品清單。此應用程式可使用 Room 更新、編輯及刪除應用程式資料庫中的項目。

2. 範例應用程式總覽

本程式碼研究室會使用先前程式碼研究室的 Inventory 應用程式解決方案程式碼,做為範例程式碼。範例應用程式已使用 Room 持續性程式庫儲存資料。使用者可以透過「Add Item」畫面,將資料新增至應用程式資料庫。

注意:目前版本的範例應用程式不會顯示資料庫中儲存的日期。

771c6a677ecd96c7.png

在本程式碼研究室中,您將擴充應用程式,使其使用 Room 程式庫讀取及顯示資料庫中的資料,以及更新及刪除資料庫中的實體。

下載這個程式碼研究室的範例程式碼

這個範例程式碼與先前程式碼研究室的解決方案程式碼相同。

如要取得本程式碼研究室的程式碼,並在 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」工具視窗中瀏覽專案檔案,瞭解應用程式的實作方式。

3. 新增 RecyclerView

在這項工作中,您要在應用程式中新增 RecyclerView,以顯示資料庫中儲存的資料。

新增輔助函式來設定價格格式

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

d6e7b7b9f12e7a16.png

請注意,價格會以貨幣格式顯示。如要將雙值換算成所需貨幣格式,請在 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")
}

在這個步驟中,您要將商品價格轉換為貨幣格式字串。一般而言,建議您不要只是為了設定資料格式而去變更代表資料的實體類別 (請參見單一責任原則),您可改為新增擴充功能函式。

  1. 在 Item.kt 的類別定義下方,新增名為 Item.getFormattedPrice() 的擴充功能函式,該函式不使用參數並且會傳回 String。請注意函式名稱中的類別名稱和點號標記。
fun Item.getFormattedPrice(): String =
   NumberFormat.getCurrencyInstance().format(itemPrice)

請在 Android Studio 出現提示時匯入 java.text.NumberFormat。

新增 ListAdapter

在這個步驟中,您要在 RecyclerView 中新增清單轉接器。鑒於您熟悉了之前程式碼研究室中有關實作轉接器的內容,操作說明匯總如下。為了方便起見,這個步驟最後提供了完整的 ItemListAdapter 檔案,可協助您瞭解程式碼研究室中的 Room 概念。

  1. 在 com.example.inventory 套件中,新增名為 ItemListAdapter 的 Kotlin 類別。傳入名為 onItemClicked() 的函式做為建構函式參數,並將 Item 物件做為參數使用。
  2. 變更 ItemListAdapter 類別簽名來擴充 ListAdapter。傳入 Item 和 ItemListAdapter.ItemViewHolder 做為參數。
  3. 新增建構函式參數 DiffCallback;ListAdapter 會使用這項參數來偵測清單中的變更內容。
  4. 覆寫必要的方法 onCreateViewHolder() 和 onBindViewHolder()。
  5. onCreateViewHolder() 方法會在 RecyclerView 需要時傳回新的 ViewHolder。
  6. 在 onCreateViewHolder() 方法中,建立一個新的 View,並使用自動產生的繫結類別 ItemListItemBinding 從 item_list_item.xml 版面配置檔案加載它。
  7. 實作 onBindViewHolder() 方法。使用 getItem() 方法取得目前商品,並傳遞位置。
  8. 在 itemView 上設定點按事件監聽器,在事件監聽器中呼叫函式 onItemClicked()。
  9. 定義 ItemViewHolder 類別,並從 RecyclerView.ViewHolder. 擴充該類別。覆寫 bind() 函式,並傳入 Item 物件。
  10. 定義夥伴物件。在伴生物件中,定義類型為 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。

9c416f2fbf1e5ae2.png

  1. 在 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,以便使用您在上一個步驟中建立的清單轉接程式,在畫面中顯示商品詳細資料。

  1. 在 InventoryViewModel 類別的開頭,針對資料庫中的商品,建立一個名為 allItems 且類型為 LiveData<List<Item>> 的 val。如果出現錯誤,別擔心,您將很快進行修正。
val allItems: LiveData<List<Item>>

請在 Android Studio 出現提示時匯入 androidx.lifecycle.LiveData。

  1. 在 itemDao 上呼叫 getItems(),並指派給 allItems。getItems() 函式會傳回 Flow。如要以 LiveData 值的形式使用資料,請使用 asLiveData() 函式。已完成的定義應如下所示:
val allItems: LiveData<List<Item>> = itemDao.getItems().asLiveData()

請在 Android Studio 出現提示時匯入 androidx.lifecycle.asLiveData。

  1. 在 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。

  1. 同樣在 ItemListFragment 中,捲動至函式 onViewCreated()。在 super.onViewCreated() 呼叫下方,宣告名為 adapter 的 val。使用預設建構函式初始化新的 adapter 屬性,ItemListAdapter{} 不會傳入任何內容。
  2. 將新建立的 adapter 繫結至 recyclerView,如下所示:
val adapter = ItemListAdapter {
}
binding.recyclerView.adapter = adapter
  1. 設定轉接器後,仍位於 onViewCreated() 內。在 allItems 上附加觀察器,監聽資料變更。
  2. 在觀察器內,對 adapter 呼叫 submitList(),並傳遞新的清單。此操作會使用清單中的新商品更新 RecyclerView。
viewModel.allItems.observe(this.viewLifecycleOwner) { items ->
   items.let {
       adapter.submitList(it)
   }
}
  1. 確認已完成的 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)
   }
}

9c416f2fbf1e5ae2.png

4. 顯示商品詳細資料

在這項工作中,您將在「Item Details」畫面中讀取並顯示實體詳細資料。您要使用主鍵 (也就是商品 id) 來從商品目錄應用程式資料庫中讀取詳細資料 (例如名稱、價格和數量),並使用 fragment_item_detail.xml 版面配置檔案將這些詳細資料顯示在「Item Details」畫面上。我們已預先為您設計好版面配置檔案 (fragment_item_detail.xml),當中包含三個用來顯示商品詳細資料的 TextView。

d699618f5d9437df.png

在這項工作中,您將執行下列步驟:

  • 在 RecyclerView 中新增點按處理常式,將應用程式移至「Item Details」畫面。
  • 在 ItemListFragment 片段中,從資料庫擷取並顯示資料。
  • 將 TextView 繫結至 ViewModel 資料。

新增點按處理常式

  1. 在 ItemListFragment 中,捲動至 onViewCreated() 函式,即可更新轉接器定義。
  2. 將 lambda 新增為 ItemListAdapter{} 的建構函式參數。
val adapter = ItemListAdapter {
}
  1. 在 lambda 中,建立名為 action 的 val。您很快便要修正初始化錯誤。
val adapter = ItemListAdapter {
    val action
}
  1. 對 ItemListFragmentDirections 呼叫 actionItemListFragmentToItemDetailFragment() 方法,傳入商品 id。將傳回的 NavDirections 物件指派給 action。
val adapter = ItemListAdapter {
   val action =    ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
}
  1. 在 action 定義下方,使用 this.findNavController() 擷取 NavController 執行個體,然後對其呼叫 navigate(),傳入 action。轉接器的定義如下所示:
val adapter = ItemListAdapter {
   val action =   ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
   this.findNavController().navigate(action)
}
  1. 執行應用程式。在 RecyclerView 中按一下某個商品。應用程式將轉至「Item Details」畫面。請注意,詳細資料為空白。輕觸按鈕,系統沒有回應。

196553111ee69beb.png

在接下來的步驟中,您要在「Item Details」畫面中顯示實體詳細資料,並為「Sell」和「Delete」按鈕新增功能。

擷取商品詳細資料

在這個步驟中,您將在 InventoryViewModel 中新增函式,以根據商品 id 從資料庫中擷取商品詳細資料。在下一個步驟中,您將使用此函式在「Item Details」畫面中顯示實體詳細資料。

  1. 在 InventoryViewModel 中,新增名為 retrieveItem() 的函式,該函式會以 Int 做為項目 ID,並傳回 LiveData<Item>。您很快便要修正傳回運算式錯誤。
fun retrieveItem(id: Int): LiveData<Item> {
}
  1. 在新函式中,對 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 中的資料,以便在資料庫中的基礎資料有所變更時,隨時更新畫面上的商品目錄清單。

  1. 在 ItemDetailFragment 中,新增類型 Item 實體的 item 可變動屬性。這個屬性可用來儲存單一實體的相關資訊。這個屬性稍後會初始化,請在前面加上 lateinit。
lateinit var item: Item

請在 Android Studio 出現提示時匯入 com.example.inventory.data.Item。

  1. 請於類別 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。

  1. 同樣在 ItemDetailFragment 中,建立名為 bind() 的 private 函式,該函式會採用 Item 實體的執行個體做為參數,並且不會傳回任何內容。
private fun bind(item: Item) {
}
  1. 實作 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()
}
  1. 更新 bind() 函式,以對程式碼區塊使用 apply{} 範圍函式,如下所示。
private fun bind(item: Item) {
   binding.apply {
       itemName.text = item.itemName
       itemPrice.text = item.getFormattedPrice()
       itemCount.text = item.quantityInStock.toString()
   }
}
  1. 還是在 ItemDetailFragment 中,覆寫 onViewCreated()。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
}
  1. 在之前某個步驟中,您已從 ItemListFragment 將商品 ID 做為導覽引數傳遞至 ItemDetailFragment 在 onViewCreated() 中的超類別函式呼叫下方,建立名為 id 的不可變變數。擷取瀏覽引數並將其指派給這個新的變數。
val id = navigationArgs.itemId
  1. 接著,請使用這個 id 變數來擷取商品詳細資料。同樣在 onViewCreated() 中,對傳入 id 的 viewModel 呼叫 retrieveItem() 函式。將觀察器附加至傳入 viewLifecycleOwner 和 lambda 的傳回值。
viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) {
   }
  1. 在 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)
   }
}
  1. 執行應用程式。按一下「Inventory」畫面上的任何清單元素,系統會隨即顯示「Item Details」畫面。請注意,現在畫面不再為空白,而是顯示擷取自商品目錄資料庫的實體詳細資料。

  1. 依序輕觸「Sell」按鈕、「Delete」按鈕和懸浮動作按鈕 (FAB)。沒有回應!接下來的工作中,您將實作這些按鈕的功能。

5. 實作銷售商品

在這項工作中,您要擴充應用程式功能,並實作銷售功能。以下是這個步驟的詳細操作說明。

  • 在 ViewModel 中新增函式以更新實體
  • 建立一個新方法,用於減少數量,同時更新應用程式資料庫中的實體。
  • 將點按事件監聽器附加到「Sell」按鈕
  • 如果數量為零,請停用「Sell」按鈕。

編寫程式碼:

  1. 在 InventoryViewModel 中,新增名為 updateItem() 的私人函式,該函式會使用實體類別 Item 的執行個體且不會傳回任何內容。
private fun updateItem(item: Item) {
}
  1. 實作新方法:updateItem()。如要從 ItemDao 類別呼叫 update() 停權方法,請使用 viewModelScope 啟動協同程式。在啟動區塊中,對傳入 item 的 itemDao 呼叫 update() 函式。已完成的方法應如下所示。
private fun updateItem(item: Item) {
   viewModelScope.launch {
       itemDao.update(item)
   }
}
  1. 依舊在 InventoryViewModel 中,新增其他名為 sellItem() 的方法,該方法會使用 Item 實體類別的執行個體,且不會傳回任何內容。
fun sellItem(item: Item) {
}
  1. 在 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)
  1. 返回 InventoryViewModel 中的 sellItem() 函式。在 if 區塊中,建立名為 newItem 的新不可變屬性。在傳入更新的 quantityInStock 的 item 例項上呼叫 copy() 函式,藉此減少 1 庫存。
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
  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)
   }
}
  1. 如要新增銷售庫存功能,請前往 ItemDetailFragment。捲動至 bind() 函式的結尾。在 apply 區塊內,為「Sell」按鈕設定點擊事件監聽器,並在 viewModel 上呼叫 sellItem() 函式。
private fun bind(item: Item) {
binding.apply {

...
    sellItem.setOnClickListener { viewModel.sellItem(item) }
    }
}
  1. 執行應用程式。在「Inventory」畫面中,按一下數量大於零的清單元素。系統隨即顯示「Item Details」畫面。輕觸「Sell」按鈕。請注意數量值會減少一個單位。

aa63ca761dc8f009.png

  1. 在「Item Details」畫面上,連續輕觸「Sell」按鈕即可將數量設為 0 (提示:選取存貨較少的實體,或建立數量較少的新實體)。數量設為 0 後,輕觸「Sell」按鈕。不會出現明顯的變更。這是因為函式 sellItem() 會先檢查數量是否大於零,然後再更新數量。

3e099d3c55596938.png

  1. 如要為使用者提供更符合需求的意見回饋,建議您在沒有商品可銷售時停用「Sell」(銷售) 按鈕。在 InventoryViewModel 中,新增函式,檢查數量是否大於 0。將函式命名為 isStockAvailable(),該函式使用 Item 例項並傳回 Boolean。
fun isStockAvailable(item: Item): Boolean {
   return (item.quantityInStock > 0)
}
  1. 前往 ItemDetailFragment,捲動至 bind() 函式。在套用區塊中,在傳入 item 的 viewModel 上呼叫 isStockAvailable() 函式。請將傳回的值設為「Sell」(銷售) 按鈕的 isEnabled 屬性。程式碼應如下所示。
private fun bind(item: Item) {
   binding.apply {
       ...
       sellItem.isEnabled = viewModel.isStockAvailable(item)
       sellItem.setOnClickListener { viewModel.sellItem(item) }
   }
}
  1. 執行應用程式,請注意,當庫存數量為 0 時,系統將停用「Sell」按鈕。恭喜您在應用程式中實作了銷售商品功能。

5e49db8451e77c2b.png

刪除商品實體

與上一項工作一樣,您將實作刪除功能,進一步擴充應用程式的功能。以下是這個步驟的詳細操作說明,相比於實作銷售功能更加輕鬆。

  • 在 ViewModel 中新增函式,以從資料庫中刪除實體
  • 在 ItemDetailFragment 中新增方法,以呼叫新的刪除函式並處理導覽。
  • 將點擊事件監聽器附加到「Delete」按鈕。

繼續編寫程式碼:

  1. 在 InventoryViewModel 中,新增名為 deleteItem() 的函式,該函式會使用名為 item 的 Item 實體類別的執行個體,但不會傳回任何內容。在 deleteItem() 函式中,使用 viewModelScope 啟動協同程式。在 launch 區塊內,對傳入 item 的 itemDao 呼叫 delete() 方法。
fun deleteItem(item: Item) {
   viewModelScope.launch {
       itemDao.delete(item)
   }
}
  1. 在 ItemDetailFragment 中,捲動至 deleteItem() 函式的開頭。對 viewModel 呼叫 deleteItem(),並傳入 item。item 例項會包含目前顯示在「Item Details」畫面上的實體。已完成的方法應如下所示。
private fun deleteItem() {
   viewModel.deleteItem(item)
   findNavController().navigateUp()
}
  1. 還是在 ItemDetailFragment 中,捲動至 showConfirmationDialog() 函式。這個函式會在範例程式碼中提供給您。這個方法會顯示快訊對話方塊,以便在刪除商品前取得使用者的確認,並在使用者輕觸肯定按鈕時呼叫 deleteItem() 函式。
private fun showConfirmationDialog() {
        MaterialAlertDialogBuilder(requireContext())
            ...
            .setPositiveButton(getString(R.string.yes)) { _, _ ->
                deleteItem()
            }
            .show()
    }

showConfirmationDialog() 函式會顯示快訊對話方塊,如下所示:

728bfcbb997c8017.png

  1. 在 ItemDetailFragment 中,請在 bind() 函式結尾的 apply 區塊內,將設定 Delete 按鈕的點擊事件監聽器。呼叫點按事件監聽器 lambda 中的 showConfirmationDialog()。
private fun bind(item: Item) {
   binding.apply {
       ...
       deleteItem.setOnClickListener { showConfirmationDialog() }
   }
}
  1. 執行應用程式!在「Inventory」清單畫面中選取清單元素,然後在「Item Details」畫面中輕觸「Delete」按鈕。輕觸「Yes」,應用程式將返回「Inventory」畫面。請注意,已刪除的實體已不在應用程式資料庫中。恭喜您實作了刪除功能。

c05318ab8c216fa1.png

編輯商品實體

與先前的工作類似,在這項工作中,您要在應用程式中新增另一項增強功能。您將實作編輯商品實體。

以下為編輯應用程式資料庫中實體的快速步驟:

  • 將片段標題更新為「編輯商品」,藉此重複使用「Add Item」(新增商品) 畫面。
  • 將點擊事件監聽器新增至懸浮動作按鈕 (FAB),前往「Edit Item」畫面。
  • 為 TextView 填入實體詳細資料。
  • 使用 Room 更新資料庫中的實體。

將點按事件監聽器新增至懸浮動作按鈕 (FAB)

  1. 在 ItemDetailFragment 中,新增名為 editItem() 的 private 函式,該函式不採用任何參數,且不會傳回任何內容。在下一個步驟中,請將畫面標題更新為「Edit Item」,這樣就能重複使用 fragment_add_item.xml。為實作此操作,您需要傳送片段標題字串以及商品 ID。
private fun editItem() {
}

更新片段標題後,「Edit Item」畫面應顯示如下。

bcd407af7c515a21.png

  1. 在 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)
}
  1. 還是在 ItemDetailFragment 中,捲動至 bind() 函式。在 apply 區塊內,為懸浮動作按鈕 (FAB) 設定點擊事件監聽器,以及從 lambda 呼叫 editItem() 函式,以便前往「Edit Item」畫面。
private fun bind(item: Item) {
   binding.apply {
       ...
       editItem.setOnClickListener { editItem() }
   }
}
  1. 執行應用程式。前往「Item Details」畫面。按一下懸浮動作按鈕 (FAB)。請注意,螢幕標題已更新為「編輯商品」,但所有文字欄位均為空白。在下一個步驟中,您將修正這個問題。

a6a6583171b68230.png

填入 TextView

在這個步驟中,您要在「Edit Item」螢幕的文字欄位中填入實體詳細資料。由於我們使用的是 Add Item 螢幕,因此您要在 Kotlin 檔案 AddItemFragment.kt 中新增函式。

  1. 在 AddItemFragment 中,新增 private 函式,將文字欄位與實體詳細資料繫結在一起。為函式 bind() 命名,該函式會使用商品實體類別的例項,且不會傳回任何內容。
private fun bind(item: Item) {
}
  1. bind() 函式的實作方式與先前在 ItemDetailFragment 中的實作類似。在 bind() 函式中,使用 format() 函式將價格四捨五入至小數點後兩位,並指派給名為 price 的 val,如下所示。
val price = "%.2f".format(item.itemPrice)
  1. 在 price 定義下方,針對 binding 屬性使用 apply 範圍函式,如下所示。
binding.apply {
}
  1. 在 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。

  1. 類似於上述步驟,設定價格 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)
   }
}
  1. 在 AddItemFragment 中,捲動至 onViewCreated() 函式。在呼叫超級類別函式之後。建立名為 id 的 val,並從導覽引數擷取 itemId。
val id = navigationArgs.itemId
  1. 新增帶有條件的 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()
       }
   }
}
  1. 執行應用程式,前往「Item Details」,然後輕觸「+」這個懸浮動作按鈕 (FAB)。請注意,這些欄位會填入商品詳細資料。編輯庫存數量或任何其他欄位,然後輕觸「Save」按鈕。沒有回應!這是因為您並未更新應用程式資料庫中的實體。您很快就會解決這個問題。

829ceb9dd7993215.png

使用 Room 更新實體

在最後一項工作中,新增最後一段程式碼來實作更新功能。您要在 ViewModel 中定義必要的函式,並在 AddItemFragment 中使用它們。

又要編寫程式碼了。

  1. 在 InventoryViewModel 中,新增名為 getUpdatedItemEntry() 的 private 函式,該函式使用 Int,以及實體詳細資料三個名為 itemName、itemPrice 和 itemCount 的字串。傳回函式的 Item 例項。程式碼僅供參考。
private fun getUpdatedItemEntry(
   itemId: Int,
   itemName: String,
   itemPrice: String,
   itemCount: String
): Item {
}
  1. 在 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()
   )
}
  1. 同樣在 InventoryViewModel 中,新增其他名為 updateItem() 的函式。此函式也會使用 Int 和實體詳細資料的三個字串,且不會傳回任何內容。使用以下程式碼片段中的變數名稱。
fun updateItem(
   itemId: Int,
   itemName: String,
   itemPrice: String,
   itemCount: String
) {
}
  1. 在 updateItem() 函式中,呼叫傳入實體資訊的 getUpdatedItemEntry() 函式,該函式會以函式參數的形式傳遞,如下所示。將傳回的值指派給名為 updatedItem 的不可變變數。
val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
  1. 在 getUpdatedItemEntry() 函式呼叫的正下方,呼叫傳入 updatedItem 的 updateItem() 函式。已完成的函式如下所示:
fun updateItem(
   itemId: Int,
   itemName: String,
   itemPrice: String,
   itemCount: String
) {
   val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
   updateItem(updatedItem)
}
  1. 返回 AddItemFragment,新增名為 updateItem() 的私人函式,該函式不含任何參數,且不會傳回任何內容。在函式中,新增 if 條件,以透過呼叫函式 isEntryValid() 來驗證使用者輸入內容。
private fun updateItem() {
   if (isEntryValid()) {
   }
}
  1. 在 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()
)
  1. 在 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)
   }
}
  1. 還是在 AddItemFragment 中,捲動至 bind() 函式。在 binding.apply 範圍函式區塊中,為「Save」按鈕設定點按事件監聽器。呼叫 lambda 中的 updateItem() 函式,如下所示。
private fun bind(item: Item) {
   ...
   binding.apply {
       ...
       saveAction.setOnClickListener { updateItem() }
   }
}
  1. 執行應用程式!嘗試編輯商品目錄商品;您應該可以編輯商品目錄應用程式資料庫中的任何商品。

1bbd094a77c25fc4.png

恭喜您成功建立第一款應用程式,能夠使用 Room 管理應用程式的資料庫!

6. 解決方案程式碼

本程式碼研究室的解決方案程式碼位於以下所示的 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」工具視窗中瀏覽專案檔案,瞭解應用程式的實作方式。

7. 摘要

  • Kotlin 能夠使用新功能擴充類別,無需沿用類別或修改類別的現有定義。方法是透過名為擴充功能的特殊宣告來執行。
  • 如要以 LiveData 值的形式使用 Flow 資料,請使用 asLiveData() 函式。
  • 根據預設,系統會向資料類別的所有執行個體提供 copy() 函式。這樣您便能複製物件並變更其部分屬性,而保留其餘的屬性。

8. 瞭解詳情

Android 開發人員說明文件

API 參照

Kotlin 參考資料