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

在這項工作中,您要更新 InventoryViewModelItemListFragment,以便使用您在上一個步驟中建立的清單轉接程式,在畫面中顯示商品詳細資料。

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

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

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

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

  1. ItemListFragment 中,請於類別開頭宣告一個類型為 InventoryViewModelprivate 不可變屬性 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() 呼叫下方,宣告名為 adapterval。使用預設建構函式初始化新的 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 中,建立名為 actionval。您很快便要修正初始化錯誤。
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(),並傳入參數 idgetItem() 函式會傳回 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 開頭宣告一個類型為 InventoryViewModelprivate 不可變屬性 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 做為導覽引數傳遞至 ItemDetailFragmentonViewCreated() 中的超類別函式呼叫下方,建立名為 id 的不可變變數。擷取瀏覽引數並將其指派給這個新的變數。
val id = navigationArgs.itemId
  1. 接著,請使用這個 id 變數來擷取商品詳細資料。同樣在 onViewCreated() 中,對傳入 idviewModel 呼叫 retrieveItem() 函式。將觀察器附加至傳入 viewLifecycleOwner 和 lambda 的傳回值。
viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) {
   }
  1. 在 lambda 中,傳入 selectedItem 做為參數,其中包含擷取自資料庫的 Item 實體。在 lambda 函式內文中,將 selectedItem 值指派給 item。呼叫傳入 itembind() 函式。已完成的函式應如下所示。
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 啟動協同程式。在啟動區塊中,對傳入 itemitemDao 呼叫 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 的新不可變屬性。在傳入更新的 quantityInStockitem 例項上呼叫 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() 函式。在套用區塊中,在傳入 itemviewModel 上呼叫 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() 的函式,該函式會使用名為 itemItem 實體類別的執行個體,但不會傳回任何內容。在 deleteItem() 函式中,使用 viewModelScope 啟動協同程式。在 launch 區塊內,對傳入 itemitemDao 呼叫 delete() 方法。
fun deleteItem(item: Item) {
   viewModelScope.launch {
       itemDao.delete(item)
   }
}
  1. ItemDetailFragment 中,捲動至 deleteItem() 函式的開頭。對 viewModel 呼叫 deleteItem(),並傳入 itemitem 例項會包含目前顯示在「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 和商品 idItemDetailFragmentDirections 呼叫 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() 函式將價格四捨五入至小數點後兩位,並指派給名為 priceval,如下所示。
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() 函式。在呼叫超級類別函式之後。建立名為 idval,並從導覽引數擷取 itemId
val id = navigationArgs.itemId
  1. 新增帶有條件的 if-else 區塊,以檢查 id 是否大於零,並將「Save」按鈕的點擊事件監聽器移至 else 區塊。if 區塊內,使用 id 擷取實體,然後在其中新增觀察器。在觀察器內部,更新 item 屬性並呼叫傳入 itembind()。系統會提供完整函式供您複製貼上。這個函式簡單易懂,您可以自行研究。
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,以及實體詳細資料三個名為 itemNameitemPriceitemCount 的字串。傳回函式的 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() 函式呼叫的正下方,呼叫傳入 updatedItemupdateItem() 函式。已完成的函式如下所示:
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() 函式呼叫下方,定義名為 actionval。對 AddItemFragmentDirections 呼叫 actionAddItemFragmentToItemListFragment(),並將傳回的值指派給 action。請瀏覽至 ItemListFragment,並呼叫傳入 actionfindNavController().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 參考資料