使用 Room 读取和更新数据

在之前的 Codelab 中,您已学过如何使用 Room 持久性库(SQLite 数据库之上的一个抽象层)来存储应用数据。在此 Codelab 中,您将为 Inventory 应用添加更多功能,并了解如何使用 Room 读取、显示、更新和删除 SQLite 数据库中的数据。您将使用 RecyclerView 显示数据库中的数据,并在数据库中的底层数据发生更改时自动更新数据。

前提条件

  • 了解如何使用 Room 库创建 SQLite 数据库并与之交互。
  • 了解如何创建实体、DAO 和数据库类。
  • 了解如何使用数据访问对象 (DAO) 将 Kotlin 函数映射到 SQL 查询。
  • 了解如何在 RecyclerView 中显示列表项。
  • 学过本单元中的上一个 Codelab:使用 Room 持久保留数据

学习内容

  • 如何读取和显示 SQLite 数据库中的实体?
  • 如何使用 Room 库更新和删除 SQLite 数据库中的实体?

构建内容

  • 您将构建一个 Inventory 应用,用于显示商品目录商品的列表。该应用可以使用 Room 更新、修改和删除应用数据库中的商品。

此 Codelab 使用上一个 Codelab 中的 Inventory 应用解决方案代码作为起始代码。起始应用已在使用 Room 持久性库保存数据。用户可以使用 Add Item 屏幕将数据添加到应用数据库中。

注意:当前版本的起始应用不会显示数据库中存储的数据。

771c6a677ecd96c7.png

在此 Codelab 中,您将扩展该应用,以便使用 Room 库读取和显示数据库中的数据,以及更新和删除数据库中的实体。

下载此 Codelab 的起始代码

此起始代码与上一个 Codelab 中的解决方案代码相同。

如需获取此 Codelab 的代码并在 Android Studio 中打开它,请执行以下操作。

获取代码

  1. 点击提供的网址。此时,项目的 GitHub 页面会在浏览器中打开。
  2. 在项目的 GitHub 页面上,点击 Code 按钮,以打开一个对话框。

5b0a76c50478a73f.png

  1. 在对话框中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
  2. 在计算机上找到该文件(可能在 Downloads 文件夹中)。
  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 对话框中,转到解压缩的项目文件夹所在的位置(可能在 Downloads 文件夹中)。
  2. 双击该项目文件夹。
  3. 等待 Android Studio 打开项目。
  4. 点击 Run 按钮 11c34fc5e516fb1c.png 以构建并运行应用。请确保该应用可以正常使用。
  5. Project 工具窗口中浏览项目文件,了解应用的实现方式。

在此任务中,您将向应用添加一个 RecyclerView 来显示存储在数据库中的数据。

添加辅助函数以设置价格格式

以下是最终应用的屏幕截图。

d6e7b7b9f12e7a16.png

请注意,价格以货币格式显示。为了将双精度值转换为所需的货币格式,您要向 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")
}

在此步骤中,您要将商品价格的格式设置为货币格式字符串。通常情况下,我们并不愿意只为设置数据格式而更改表示数据的实体类(请参阅单一责任原则),因此会改为添加扩展函数。

  1. Item.kt 中的类定义下,添加一个名为 Item.getFormattedPrice() 的扩展函数,该函数不带参数并返回一个 String。请注意函数名称中的类名称和点分表示法。
fun Item.getFormattedPrice(): String =
   NumberFormat.getCurrencyInstance().format(itemPrice)

当 Android Studio 提示时,导入 java.text.NumberFormat

添加 ListAdapter

在此步骤中,您将向 RecyclerView 添加一个列表适配器。由于您已在之前的 Codelab 中熟悉了如何实现适配器,因此我们在下面对相关说明进行了总结。为了方便您查阅也为了帮助您对此 Codelab 中的 Room 概念增进了解,此步骤末尾提供了完成后的 ItemListAdapter 文件。

  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,使用自动生成的绑定类 ItemListItemBinding 通过 item_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
           }
       }
   }
}

观察完成后的应用(此 Codelab 末尾的解决方案应用)中的商品目录屏幕。请注意,每个列表元素都会显示相应商品目录商品的名称、货币格式的价格和当前的现货库存。在前面的步骤中,您曾使用包含三个 TextView 的 item_list_item.xml 布局文件创建行。在下一步中,您要将实体详细信息绑定到这些 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

在此任务中,您将使用您在上一步中创建的列表适配器更新 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 中的类开头,声明一个名为 viewModel 且类型为 InventoryViewModelprivate 不可变属性。使用 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。使用默认构造函数 ItemListAdapter{} 初始化新的 adapter 属性,不传入任何内容。
  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

在此任务中,您将读取并在 Item Details 屏幕上显示实体详细信息。您将使用主键(商品 id)从 Inventory 应用数据库中读取名称、价格和数量等详细信息,并使用 fragment_item_detail.xml 布局文件将其显示于 Item Details 屏幕上。布局文件 fragment_item_detail.xml 已为您预先设计好,包含三个用于显示商品详情的 TextView。

d699618f5d9437df.png

您将在此任务中执行以下步骤:

  • 向 RecyclerView 添加一个点击处理程序,用于将应用转到 Item Details 屏幕。
  • ItemListFragment fragment 中,从数据库检索并显示数据。
  • 将 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() 的函数,该函数接受一个用于表示商品 ID 的 Int 并返回一个 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 类的开头,声明一个名为 viewModel 且类型为 InventoryViewModelprivate 不可变属性。使用 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. 在前面的一个步骤中,您将商品 ID 作为导航参数从 ItemListFragment 传递给 ItemDetailFragment。在 onViewCreated() 内的超类函数调用下,创建一个名为 id 的不可变变量。检索导航参数并将其赋值给此新变量。
val id = navigationArgs.itemId
  1. 现在,您将使用此 id 变量来检索商品详情。仍在 onViewCreated() 内,对 viewModel 调用 retrieveItem() 函数,并传入 id。将一个观察器附加到返回的值,并传入 viewLifecycleOwner 和 lambda。
viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) {
   }
  1. 在 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)
   }
}
  1. 运行应用。点击 Inventory 屏幕上的任意列表元素,系统将显示 Item Details 屏幕。请注意,屏幕现在不再空白一片,而是显示从商品目录数据库中检索到的实体详细信息。

  1. 点按 SellDelete 和 FAB 按钮。毫无反应!在后续任务中,您将实现这些按钮的功能。

在此任务中,您将扩展应用的功能以实现销售功能。下面是此步骤的简要说明。

  • 在 ViewModel 中添加一个函数来更新实体。
  • 创建一个新方法来减少数量并更新应用数据库中的实体。
  • 将一个点击监听器附加到 Sell 按钮。
  • 如果数量为零,停用 Sell 按钮。

让我们来编写代码:

  1. InventoryViewModel 中,添加一个名为 updateItem() 的私有函数,该函数接受实体类 Item 的实例并且不返回任何内容。
private fun updateItem(item: Item) {
}
  1. 实现新方法 updateItem()。如需从 ItemDao 类调用 update() 挂起方法,请使用 viewModelScope 启动协程。在 launch 块内,对 itemDao 调用 update() 函数,并传入 item。完成后的方法应如下所示。
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 的不可变属性。对 item 实例调用 copy() 函数,并传入更新后的 quantityInStock,即库存数减 1
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
  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)
   }
}
  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。(提示:选择一个库存较少的实体,或者创建一个数量较少的新实体。)数量为零后,点按 Sell 按钮。没有任何可见的变化。这是因为函数 sellItem() 会在更新数量前检查数量是否大于零。

3e099d3c55596938.png

  1. 为了向用户提供更好的反馈,您可能需要在没有可供销售的商品时停用 Sell 按钮。在 InventoryViewModel 中,添加一个函数来检查数量是否大于 0。将该函数命名为 isStockAvailable(),接受一个 Item 实例并返回一个 Boolean
fun isStockAvailable(item: Item): Boolean {
   return (item.quantityInStock > 0)
}
  1. 转到 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) }
   }
}
  1. 运行应用,注意当库存数量为零时,Sell 按钮处于停用状态。恭喜您为应用实现了商品销售功能。

5e49db8451e77c2b.png

删除 item 实体

与上一个任务类似,您将实现删除功能,进一步扩展应用功能。下面是此步骤的简要说明,比实现销售功能要容易得多。

  • 在 ViewModel 中添加一个函数来删除数据库中的实体
  • ItemDetailFragment 中添加一个新方法,以调用新的删除函数并处理导航。
  • 将一个点击监听器附加到 Delete 按钮。

我们来继续编写代码:

  1. InventoryViewModel 中,添加一个名为 deleteItem() 的新函数,该函数接受名为 itemItem 实体类实例,并且不返回任何内容。在 deleteItem() 函数内,使用 viewModelScope 启动协程。在 launch 块内,对 itemDao 调用 delete() 方法,并传入 item
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. ItemDetailFragmentbind() 函数末尾的 apply 块内,设置 Delete 按钮的点击监听器。在点击监听器 lambda 内,调用 showConfirmationDialog()
private fun bind(item: Item) {
   binding.apply {
       ...
       deleteItem.setOnClickListener { showConfirmationDialog() }
   }
}
  1. 运行应用!在 Inventory 列表屏幕上选择一个列表元素,然后在 Item Details 屏幕中点按 Delete 按钮。点按 Yes,应用将返回到 Inventory 屏幕。请注意,您删除的实体不再存在于应用数据库中。恭喜您实现了删除功能。

c05318ab8c216fa1.png

修改 item 实体

与上一个任务类似,在此任务中,您将向应用中添加另一项增强功能。您将实现修改 item 实体的功能。

下面,我们将快速过一遍修改应用数据库中实体的步骤:

  • 通过将 fragment 标题更新为 Edit Item,重复使用 Add Item 屏幕。
  • 向 FAB 添加点击监听器,用于转到 Edit Item 屏幕。
  • 使用实体详细信息填充 TextView。
  • 使用 Room 更新数据库中的实体。

向 FAB 添加点击监听器

  1. ItemDetailFragment 中,添加一个名为 editItem() 的新 private 函数,该函数不带参数也不返回任何内容。在下一步中,您要将屏幕标题更新为 Edit Item,以便重复使用 fragment_add_item.xml。为了做到这一点,您要将 fragment 标题字符串连同商品 ID 一起,作为 action 的一部分发送。
private fun editItem() {
}

更新 fragment 标题后,Edit Item 屏幕应如下所示。

bcd407af7c515a21.png

  1. 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)
}
  1. 仍在 ItemDetailFragment 内,滚动到 bind() 函数。在 apply 块内,设置 FAB 的点击监听器,从 lambda 调用 editItem() 函数以转到 dit Item 屏幕。
private fun bind(item: Item) {
   binding.apply {
       ...
       editItem.setOnClickListener { editItem() }
   }
}
  1. 运行应用。转到 Item Details 屏幕。点击 FAB。请注意,屏幕的标题已更新为 Edit Item,但所有文本字段均为空。在下一步中,您将修复此问题。

a6a6583171b68230.png

填充 TextView

在此步骤中,您将使用实体详细信息填充 Edit Item 屏幕中的文本字段。由于我们使用的是 Add Item 屏幕,因此您需要向 Kotlin 文件 AddItemFragment.kt 中添加新函数。

  1. AddItemFragment 中,添加一个新的 private 函数,以将文本字段与实体详细信息绑定。将该函数命名为 bind(),接受 Item 实体类的实例并且不返回任何内容。
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 的 text 属性。使用 setText() 函数,并传入 item.itemName 字符串和作为 BufferTypeTextView.BufferType.SPANNABLE
binding.apply {
   itemName.setText(item.itemName, TextView.BufferType.SPANNABLE)
}

如果 Android Studio 提示,请导入 android.widget.TextView

  1. 与上面的步骤类似,设置价格 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)
   }
}
  1. 仍在 AddItemFragment 内,滚动到 onViewCreated() 函数。在超类函数的调用后,创建一个名为 idval,并从导航参数中检索 itemId
val id = navigationArgs.itemId
  1. 添加一个 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()
       }
   }
}
  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 实例,如下所示。从该函数返回 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() 函数调用下,调用 updateItem() 函数,并传入 updatedItem。完成后的函数如下所示:
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,调用 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)
   }
}
  1. 仍在 AddItemFragment 内,滚动到 bind() 函数。在 binding.apply 作用域函数块内,为 Save 按钮设置点击监听器。在 lambda 内调用 updateItem() 函数,如下所示。
private fun bind(item: Item) {
   ...
   binding.apply {
       ...
       saveAction.setOnClickListener { updateItem() }
   }
}
  1. 运行应用!请尝试修改商品目录商品;您应该能够修改 Inventory 应用数据库中的任何商品。

1bbd094a77c25fc4.png

恭喜您创建了自己的第一个使用 Room 管理应用数据库的应用!

此 Codelab 的解决方案代码位于下方所示的 GitHub 代码库和分支中。

如需获取此 Codelab 的代码并在 Android Studio 中打开它,请执行以下操作。

获取代码

  1. 点击提供的网址。此时,项目的 GitHub 页面会在浏览器中打开。
  2. 在项目的 GitHub 页面上,点击 Code 按钮,以打开一个对话框。

5b0a76c50478a73f.png

  1. 在对话框中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
  2. 在计算机上找到该文件(可能在 Downloads 文件夹中)。
  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 对话框中,转到解压缩的项目文件夹所在的位置(可能在 Downloads 文件夹中)。
  2. 双击该项目文件夹。
  3. 等待 Android Studio 打开项目。
  4. 点击 Run 按钮 11c34fc5e516fb1c.png 以构建并运行应用。请确保该应用可以正常使用。
  5. Project 工具窗口中浏览项目文件,了解应用的实现方式。
  • Kotlin 提供了新的功能,用于扩展类而不必继承类或修改类的现有定义。此功能通过称为扩展的特殊声明实现。
  • 如需将 Flow 数据用作 LiveData 值,请使用 asLiveData() 函数。
  • copy() 函数会默认提供给所有数据类实例。通过该函数,您可以复制对象并更改其某些属性,同时保持其余属性不变。

Android 开发者文档

API 参考文档

Kotlin 参考文档