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()
函数。将itemName
TextView 绑定到item.itemName
。使用getFormattedPrice()
扩展函数获取货币格式的价格,并将其绑定到itemPrice
TextView。将quantityInStock
值转换为String
,并将其绑定到itemQuantity
TextView。完成后的方法应如下所示。
fun bind(item: Item) {
binding.apply {
itemName.text = item.itemName
itemPrice.text = item.getFormattedPrice()
itemQuantity.text = item.quantityInStock.toString()
}
}
当 Android Studio 提示时,导入 com.example.inventory.data.getFormattedPrice
。
使用 ListAdapter
在此任务中,您将使用您在上一步中创建的列表适配器更新 InventoryViewModel
和 ItemListFragment
,以在屏幕上显示商品详情。
- 在
InventoryViewModel
类的开头,为数据库中的商品创建一个名为allItems
且类型为LiveData<List<Item>>
的val
。不用担心错误,您很快将进行修复。
val allItems: LiveData<List<Item>>
当 Android Studio 提示时,导入 androidx.lifecycle.LiveData
。
- 对
itemDao
调用getItems()
,并将其赋值给allItems
。getItems()
函数会返回一个Flow
。如需将数据用作LiveData
值,请使用asLiveData()
函数。完成后的定义应如下所示:
val allItems: LiveData<List<Item>> = itemDao.getItems().asLiveData()
当 Android Studio 提示时,导入 androidx.lifecycle.asLiveData
。
- 在
ItemListFragment
中的类开头,声明一个名为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 屏幕。
- 在
ItemListFragment
fragment 中,从数据库检索并显示数据。 - 将 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
中执行的操作类似。将itemName
TextView 的text
属性设为item.itemName
。对item
属性调用getFormattedPrice
()
以设置价格值的格式,并将其设为itemPrice
TextView 的text
属性。将quantityInStock
转换为String
,并将其设为itemQuantity
TextView 的text
属性。
private fun bind(item: Item) {
binding.itemName.text = item.itemName
binding.itemPrice.text = item.getFormattedPrice()
binding.itemCount.text = item.quantityInStock.toString()
}
- 更新
bind()
函数以将apply{}
作用域函数用于代码块,如下所示。
private fun bind(item: Item) {
binding.apply {
itemName.text = item.itemName
itemPrice.text = item.getFormattedPrice()
itemCount.text = item.quantityInStock.toString()
}
}
- 仍在
ItemDetailFragment
中,替换onViewCreated()
。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
- 在前面的一个步骤中,您将商品 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 参考文档