1. 准备工作
在之前的 Codelab 中,您已学过如何使用 Room 持久性库(SQLite 数据库之上的一个抽象层)来存储应用数据。在此 Codelab 中,您将为 Inventory 应用添加更多功能,并了解如何使用 Room 读取、显示、更新和删除 SQLite 数据库中的数据。您将使用 RecyclerView 显示数据库中的数据,并在数据库中的底层数据发生更改时自动更新数据。
前提条件
- 了解如何使用 Room 库创建 SQLite 数据库并与之交互。
 - 了解如何创建实体、DAO 和数据库类。
 - 了解如何使用数据访问对象 (DAO) 将 Kotlin 函数映射到 SQL 查询。
 - 了解如何在 
RecyclerView中显示列表项。 - 学过本单元中的上一个 Codelab:使用 Room 持久保留数据。
 
学习内容
- 如何读取和显示 SQLite 数据库中的实体。
 - 如何使用 Room 库更新和删除 SQLite 数据库中的实体。
 
您将构建的内容
- 您将构建一个 Inventory 应用,用于显示商品目录中各商品的列表。该应用可以使用 Room 更新、修改和删除应用数据库中的商品。
 
2. 起始应用概览
此 Codelab 使用上一个 Codelab 中的 Inventory 应用解决方案代码作为起始代码。起始应用已在使用 Room 持久性库保存数据。用户可以使用 Add Item 屏幕将数据添加到应用数据库中。
注意:当前版本的起始应用不会显示数据库中存储的数据。
在此 Codelab 中,您将扩展该应用,以便使用 Room 库读取和显示数据库中的数据,以及更新和删除数据库中的实体。
下载此 Codelab 的起始代码
此起始代码与上一个 Codelab 中的解决方案代码相同。
如需获取此 Codelab 的代码并在 Android Studio 中打开它,请执行以下操作。
获取代码
- 点击提供的网址。此时,项目的 GitHub 页面会在浏览器中打开。
 - 在项目的 GitHub 页面上,点击 Code 按钮,这时会出现一个对话框。
 

- 在对话框中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
 - 在计算机上找到该文件(很可能在 Downloads 文件夹中)。
 - 双击 ZIP 文件进行解压缩。系统将创建一个包含项目文件的新文件夹。
 
在 Android Studio 中打开项目
- 启动 Android Studio。
 - 在 Welcome to Android Studio 窗口中,点击 Open an existing Android Studio project。
 

注意:如果 Android Studio 已经打开,请依次选择 File > New > Import Project 菜单选项。

- 在 Import Project 对话框中,前往解压缩的项目文件夹所在的位置(很可能在 Downloads 文件夹中)。
 - 双击该项目文件夹。
 - 等待 Android Studio 打开项目。
 - 点击 Run 按钮 
 以构建并运行应用。请确保该应用可以正常使用。 - 在 Project 工具窗口中浏览项目文件,了解应用的实现方式。
 
3. 添加 RecyclerView
在此任务中,您将向应用添加一个 RecyclerView 来显示存储在数据库中的数据。
添加辅助函数以设置价格格式
以下是最终应用的屏幕截图。

请注意,价格以货币格式显示。为了将双精度值转换为所需的货币格式,您要向 Item 类添加一个扩展函数。
扩展函数
Kotlin 提供了新的功能,用于扩展类而不必继承类或修改类的现有定义。也就是说,您无需访问源代码即可向现有类添加函数。此功能通过称为扩展的特殊声明实现。
例如,您可以为您无法修改的第三方库中的类编写新函数。此类函数可以按常规方式调用,就像它们是原始类的方法一样。这些函数称为扩展函数。(此外还有扩展属性,可用于为现有类定义新属性,但这些内容不在此 Codelab 的范围内。)
扩展函数实际上并不会修改类,而是让您在对该类的对象调用此函数时可以使用点分表示法。
例如,以下代码段中有一个名为 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 添加一个列表适配器。由于您已在之前的 Codelab 中熟悉了如何实现适配器,因此我们在下面对相关说明进行了总结。为了方便您查阅也为了帮助您对此 Codelab 中的 Room 概念增进了解,此步骤末尾提供了完成后的 ItemListAdapter 文件。
- 在 
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
           }
       }
   }
}
观察完成后的应用(此 Codelab 末尾的解决方案应用)中的商品目录屏幕。请注意,每个列表元素都会显示相应商品目录商品的名称、货币格式的价格和当前的现货库存。在前面的步骤中,您曾使用包含三个 TextView 的 item_list_item.xml 布局文件创建行。在下一步中,您要将实体详细信息绑定到这些 TextView。

- 在 
ItemListAdapter.kt中,在ItemViewHolder类中实现bind()函数。将itemNameTextView 绑定到item.itemName。使用getFormattedPrice()扩展函数获取货币格式的价格,并将其绑定到itemPriceTextView。将quantityInStock值转换为String,并将其绑定到itemQuantityTextView。完成后的方法应如下所示。 
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中的类开头,声明一个名为viewModel且类型为InventoryViewModel的private不可变属性。使用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。使用默认构造函数ItemListAdapter{}初始化新的adapter属性,不传入任何内容。 - 将新创建的 
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)从 Inventory 应用数据库中读取名称、价格和数量等详细信息,并使用 fragment_item_detail.xml 布局文件在 Item Details 屏幕上显示这些信息。布局文件 fragment_item_detail.xml 是为您预先设计好的,包含三个用于显示商品详情的 TextView。

您将在此任务中执行以下步骤:
- 向 RecyclerView 添加一个点击处理程序,用于将应用转到 Item Details 屏幕。
 - 在 
ItemListFragmentfragment 中,从数据库检索并显示数据。 - 将 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()的函数,该函数接受一个用于表示商品 ID 的Int并返回一个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类的开头,声明一个名为viewModel且类型为InventoryViewModel的private不可变属性。使用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中执行的操作类似。将itemNameTextView 的text属性设为item.itemName。对item属性调用getFormattedPrice()以设置价格值的格式,并将其设为itemPriceTextView 的text属性。将quantityInStock转换为String,并将其设为itemQuantityTextView 的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)
}
- 在前面的一个步骤中,您将商品 ID 作为导航参数从 
ItemListFragment传递给ItemDetailFragment。在onViewCreated()内的超类函数调用下,创建一个名为id的不可变变量。检索导航参数并将其赋值给此新变量。 
val id = navigationArgs.itemId
- 现在,您将使用此 
id变量来检索商品详情。仍在onViewCreated()内,对viewModel调用retrieveItem()函数,并传入id。将一个观察器附加到返回的值,并传入viewLifecycleOwner和 lambda。 
viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) {
   }
- 在 lambda 内,传入 
selectedItem作为参数,该参数包含从数据库中检索的Item实体。在 lambda 函数主体中,将selectedItem值赋值给item。调用bind()函数,并传入item。完成后的函数应如下所示。 
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启动协程。在 launch 块内,对itemDao调用update()函数,并传入item。完成后的方法应如下所示。 
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的不可变属性。对item实例调用copy()函数,并传入更新后的quantityInStock,即库存数减1。 
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
- 在 
newItem的定义下,调用updateItem()函数,并传入更新后的新实体,即newItem。完成后的方法应如下所示。 
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。(提示:选择一个库存较少的实体,或者创建一个数量较少的新实体。)数量为零后,点按 Sell 按钮。没有任何可见的变化。这是因为函数 
sellItem()会在更新数量前检查数量是否大于零。 

- 为了向用户提供更好的反馈,您可能需要在没有可供销售的商品时停用 Sell 按钮。在 
InventoryViewModel中,添加一个函数来检查数量是否大于0。将该函数命名为isStockAvailable(),接受一个Item实例并返回一个Boolean。 
fun isStockAvailable(item: Item): Boolean {
   return (item.quantityInStock > 0)
}
- 转到 
ItemDetailFragment,滚动到bind()函数。在 apply 块内,对viewModel调用isStockAvailable()函数,并传入item。将返回值设为 Sell 按钮的isEnabled属性。您的代码应如下所示。 
private fun bind(item: Item) {
   binding.apply {
       ...
       sellItem.isEnabled = viewModel.isStockAvailable(item)
       sellItem.setOnClickListener { viewModel.sellItem(item) }
   }
}
- 运行应用,注意当库存数量为零时,Sell 按钮处于停用状态。恭喜您为应用实现了商品销售功能。
 

删除 item 实体
与上一个任务类似,您将实现删除功能,进一步扩展应用功能。下面是此步骤的简要说明,比实现销售功能要容易得多。
- 在 ViewModel 中添加一个函数来删除数据库中的实体
 - 在 
ItemDetailFragment中添加一个新方法,以调用新的删除函数并处理导航。 - 将一个点击监听器附加到 Delete 按钮。
 
我们来继续编写代码:
- 在 
InventoryViewModel中,添加一个名为deleteItem()的新函数,该函数接受名为item的Item实体类实例,并且不返回任何内容。在deleteItem()函数内,使用viewModelScope启动协程。在launch块内,对itemDao调用delete()方法,并传入item。 
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 屏幕。请注意,您删除的实体不再存在于应用数据库中。恭喜您实现了删除功能。
 

修改 item 实体
与上一个任务类似,在此任务中,您将向应用中添加另一项增强功能。您将实现修改 item 实体的功能。
下面,我们将快速过一遍修改应用数据库中实体的步骤:
- 通过将 fragment 标题更新为 Edit Item,重复使用 Add Item 屏幕。
 - 向 FAB 添加点击监听器,用于转到 Edit Item 屏幕。
 - 使用实体详细信息填充 TextView。
 - 使用 Room 更新数据库中的实体。
 
向 FAB 添加点击监听器
- 在 
ItemDetailFragment中,添加一个名为editItem()的新private函数,该函数不带参数也不返回任何内容。在下一步中,您要将屏幕标题更新为 Edit Item,以便重复使用fragment_add_item.xml。为了做到这一点,您要将 fragment 标题字符串连同商品 ID 一起,作为 action 的一部分发送。 
private fun editItem() {
}
更新 fragment 标题后,Edit Item 屏幕应如下所示。

- 在 
editItem()函数内,创建一个名为action的不可变变量。对ItemDetailFragmentDirections调用actionItemDetailFragmentToAddItemFragment(),并传入标题字符串edit_fragment_title和商品id。将返回的值赋值给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()函数以转到 dit Item 屏幕。 
private fun bind(item: Item) {
   binding.apply {
       ...
       editItem.setOnClickListener { editItem() }
   }
}
- 运行应用。转到 Item Details 屏幕。点击 FAB。请注意,屏幕的标题已更新为 Edit Item,但所有文本字段均为空。在下一步中,您将修复此问题。
 

填充 TextView
在此步骤中,您将使用实体详细信息填充 Edit Item 屏幕中的文本字段。由于我们使用的是 Add Item 屏幕,因此您需要向 Kotlin 文件 AddItemFragment.kt 中添加新函数。
- 在 
AddItemFragment中,添加一个新的private函数,以将文本字段与实体详细信息绑定。将该函数命名为bind(),接受 Item 实体类的实例并且不返回任何内容。 
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的 text 属性。使用setText()函数,并传入item.itemName字符串和作为BufferType的TextView.BufferType.SPANNABLE。 
binding.apply {
   itemName.setText(item.itemName, TextView.BufferType.SPANNABLE)
}
如果 Android Studio 提示,请导入 android.widget.TextView。
- 与上面的步骤类似,设置价格 
EditText的 text 属性,如下所示。如需设置数量 EditText 的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属性,调用bind()并传入item。我们提供了完整的函数供您复制和粘贴。这些内容简单易懂;请自行解读。 
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 实例,如下所示。从该函数返回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()函数调用下,调用updateItem()函数,并传入updatedItem。完成后的函数如下所示: 
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,调用findNavController().navigate()并传入action。 
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() }
   }
}
- 运行应用!请尝试修改商品目录商品;您应该能够修改 Inventory 应用数据库中的任何商品。
 

恭喜您创建了自己的第一个使用 Room 管理应用数据库的应用!
6. 解决方案代码
此 Codelab 的解决方案代码位于下方所示的 GitHub 代码库和分支中。
如需获取此 Codelab 的代码并在 Android Studio 中打开它,请执行以下操作。
获取代码
- 点击提供的网址。此时,项目的 GitHub 页面会在浏览器中打开。
 - 在项目的 GitHub 页面上,点击 Code 按钮,这时会出现一个对话框。
 

- 在对话框中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
 - 在计算机上找到该文件(很可能在 Downloads 文件夹中)。
 - 双击 ZIP 文件进行解压缩。系统将创建一个包含项目文件的新文件夹。
 
在 Android Studio 中打开项目
- 启动 Android Studio。
 - 在 Welcome to Android Studio 窗口中,点击 Open an existing Android Studio project。
 

注意:如果 Android Studio 已经打开,请依次选择 File > New > Import Project 菜单选项。

- 在 Import Project 对话框中,前往解压缩的项目文件夹所在的位置(很可能在 Downloads 文件夹中)。
 - 双击该项目文件夹。
 - 等待 Android Studio 打开项目。
 - 点击 Run 按钮 
 以构建并运行应用。请确保该应用可以正常使用。 - 在 Project 工具窗口中浏览项目文件,了解应用的实现方式。
 
7. 总结
- Kotlin 提供了新的功能,用于扩展类而不必继承类或修改类的现有定义。此功能通过称为扩展的特殊声明实现。
 - 如需将 
Flow数据用作LiveData值,请使用asLiveData()函数。 copy()函数会默认提供给所有数据类实例。通过该函数,您可以复制对象并更改其某些属性,同时保持其余属性不变。
8. 了解更多内容
Android 开发者文档
API 参考文档
Kotlin 参考文档
  
