1. 开始之前
在之前的 Codelab 中,您学习了如何使用 Room 持久性库(一个建立在 SQLite 数据库之上的抽象层)来存储应用数据。在本 Codelab 中,您将为 Inventory 应用添加更多功能,并学习如何使用 Room 从 SQLite 数据库中读取、显示、更新和删除数据。您将使用 LazyColumn
来显示数据库中的数据,并在数据库中的基础数据发生更改时自动更新数据。
先决条件
- 能够使用 Room 库创建和与 SQLite 数据库交互。
- 能够创建实体、DAO 和数据库类。
- 能够使用数据访问对象 (DAO) 将 Kotlin 函数映射到 SQL 查询。
- 能够在
LazyColumn
中显示列表项。 - 已完成本单元中的上一篇 Codelab,使用 Room 持久化数据。
您将学到什么
- 如何从 SQLite 数据库中读取和显示实体。
- 如何使用 Room 库从 SQLite 数据库中更新和删除实体。
您将构建什么
- 一个 Inventory 应用,它显示库存项目的列表,并可以使用 Room 从应用数据库中更新、编辑和删除项目。
您需要什么
- 一台装有 Android Studio 的电脑
2. 初始应用概述
本 Codelab 使用上一篇 Codelab 使用 Room 持久化数据 中 Inventory 应用的解决方案代码作为初始代码。初始应用已使用 Room 持久性库保存数据。用户可以使用“添加项目”屏幕将数据添加到应用数据库。
在本 Codelab 中,您将扩展应用以读取和显示数据,并使用 Room 库更新和删除数据库上的实体。
下载本 Codelab 的初始代码
要开始,请下载初始代码
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-inventory-app.git $ cd basic-android-kotlin-compose-training-inventory-app $ git checkout room
或者,您可以将存储库下载为 zip 文件,解压缩并将其在 Android Studio 中打开。
如果您想查看本 Codelab 的初始代码,请在 GitHub 上查看。
3. 更新 UI 状态
在本任务中,您将向应用中添加一个 LazyColumn
来显示存储在数据库中的数据。
HomeScreen 可组合函数演练
- 打开
ui/home/HomeScreen.kt
文件并查看HomeScreen()
可组合函数。
@Composable
fun HomeScreen(
navigateToItemEntry: () -> Unit,
navigateToItemUpdate: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Scaffold(
topBar = {
// Top app with app title
},
floatingActionButton = {
FloatingActionButton(
// onClick details
) {
Icon(
// Icon details
)
}
},
) { innerPadding ->
// Display List header and List of Items
HomeBody(
itemList = listOf(), // Empty list is being passed in for itemList
onItemClick = navigateToItemUpdate,
modifier = modifier.padding(innerPadding)
.fillMaxSize()
)
}
此可组合函数显示以下项目
- 显示应用标题的顶部应用栏
- 用于向库存中添加新项目的浮动操作按钮 (FAB)
- The
HomeBody()
可组合函数
The HomeBody()
可组合函数根据传入的列表显示库存项目。作为初始代码实现的一部分,一个空列表 (listOf()
) 被传递到 HomeBody()
可组合函数。要将库存列表传递到此可组合函数,您必须从存储库检索库存数据并将其传递到 HomeViewModel
中。
在 HomeViewModel
中发出 UI 状态
当您向 ItemDao
添加获取项目的方法 - getItem()
和 getAllItems()
- 时,您指定了 Flow
作为返回类型。回想一下,Flow
表示数据流。通过返回 Flow
,您只需为给定的生命周期显式调用一次 DAO 中的方法。Room 以异步方式处理基础数据的更新。
从流中获取数据称为“从流中收集”。在 UI 层从流中收集时,需要考虑以下几点。
- 生命周期事件(例如配置更改,例如旋转设备)会导致活动重新创建。这会导致重新组合并再次从您的
Flow
中收集。 - 您希望将值缓存为状态,以便在生命周期事件之间不会丢失现有数据。
- 如果不再有观察者(例如,在可组合函数的生命周期结束后),则应取消流。
从 ViewModel
公开 Flow
的推荐方法是使用 StateFlow
。使用 StateFlow
允许保存和观察数据,而不管 UI 生命周期如何。要将 Flow
转换为 StateFlow
,可以使用 stateIn
运算符。
The stateIn
运算符有三个参数,如下所述
scope
-viewModelScope
定义StateFlow
的生命周期。当viewModelScope
被取消时,StateFlow
也会被取消。started
- 管道仅在 UI 可见时才应处于活动状态。使用SharingStarted.WhileSubscribed()
来实现此目的。要在最后一个订阅者消失和共享协程停止之间配置延迟(以毫秒为单位),请将TIMEOUT_MILLIS
传递到SharingStarted.WhileSubscribed()
方法中。initialValue
- 将状态流的初始值设置为HomeUiState()
。
将您的 Flow
转换为 StateFlow
后,您可以使用 collectAsState()
方法收集它,将其数据转换为相同类型的 State
。
在此步骤中,您将检索 Room 数据库中的所有项目作为 UI 状态的可观察 API StateFlow
。当 Room Inventory 数据更改时,UI 会自动更新。
- 打开
ui/home/HomeViewModel.kt
文件,其中包含一个TIMEOUT_MILLIS
常量和一个HomeUiState
数据类,该类以项目的列表作为构造函数参数。
// No need to copy over, this code is part of starter code
class HomeViewModel : ViewModel() {
companion object {
private const val TIMEOUT_MILLIS = 5_000L
}
}
data class HomeUiState(val itemList: List<Item> = listOf())
- 在
HomeViewModel
类中,声明一个名为homeUiState
的val
,其类型为StateFlow<HomeUiState>
。您将很快解决初始化错误。
val homeUiState: StateFlow<HomeUiState>
- 在
itemsRepository
上调用getAllItemsStream()
并将其分配给您刚刚声明的homeUiState
。
val homeUiState: StateFlow<HomeUiState> =
itemsRepository.getAllItemsStream()
现在您会收到一个错误 - 未解析的引用:itemsRepository。要解决未解析的引用错误,您需要将 ItemsRepository
对象传递到 HomeViewModel
中。
- 向
HomeViewModel
类添加一个类型为ItemsRepository
的构造函数参数。
import com.example.inventory.data.ItemsRepository
class HomeViewModel(itemsRepository: ItemsRepository): ViewModel() {
- 在
ui/AppViewModelProvider.kt
文件中,在HomeViewModel
初始化器中,传递ItemsRepository
对象,如下所示。
initializer {
HomeViewModel(inventoryApplication().container.itemsRepository)
}
- 返回
HomeViewModel.kt
文件。请注意类型不匹配错误。要解决此问题,请添加如下所示的转换映射。
val homeUiState: StateFlow<HomeUiState> =
itemsRepository.getAllItemsStream().map { HomeUiState(it) }
Android Studio 仍然显示类型不匹配错误。此错误是由于 homeUiState
的类型为 StateFlow
,而 getAllItemsStream()
返回 Flow
。
- 使用
stateIn
运算符将Flow
转换为StateFlow
。StateFlow
是 UI 状态的可观察 API,它使 UI 能够自动更新自身。
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
val homeUiState: StateFlow<HomeUiState> =
itemsRepository.getAllItemsStream().map { HomeUiState(it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
initialValue = HomeUiState()
)
- 构建应用以确保代码中没有错误。不会有任何视觉变化。
4. 显示库存数据
在本任务中,您将在 HomeScreen
中收集和更新 UI 状态。
- 在
HomeScreen.kt
文件中,于HomeScreen
可组合函数中,添加一个新的函数参数,类型为HomeViewModel
并进行初始化。
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.inventory.ui.AppViewModelProvider
@Composable
fun HomeScreen(
navigateToItemEntry: () -> Unit,
navigateToItemUpdate: (Int) -> Unit,
modifier: Modifier = Modifier,
viewModel: HomeViewModel = viewModel(factory = AppViewModelProvider.Factory)
)
- 在
HomeScreen
可组合函数中,添加一个名为homeUiState
的val
用于从HomeViewModel
收集 UI 状态。你使用collectAsState
()
,它会从这个StateFlow
中收集值,并通过State
表示其最新值。
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
val homeUiState by viewModel.homeUiState.collectAsState()
- 更新
HomeBody()
函数调用,并将homeUiState.itemList
传递给itemList
参数。
HomeBody(
itemList = homeUiState.itemList,
onItemClick = navigateToItemUpdate,
modifier = modifier.padding(innerPadding)
)
- 运行应用。注意,如果在应用数据库中保存了项目,则会显示库存列表。如果列表为空,请向应用数据库中添加一些库存项目。
5. 测试数据库
之前的 codelab 讨论了测试代码的重要性。在此任务中,您将添加一些单元测试来测试您的 DAO 查询,然后在您完成 codelab 的过程中添加更多测试。
测试数据库实现的推荐方法是编写一个在 Android 设备上运行的 JUnit 测试。由于这些测试不需要创建活动,因此比 UI 测试执行速度更快。
- 在
build.gradle.kts (Module :app)
文件中,请注意 Espresso 和 JUnit 的以下依赖项。
// Testing
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
- 切换到**项目**视图,右键单击**src** > **新建** > **目录**,为您的测试创建一个测试源集。
- 从**新建目录**弹出窗口中选择**androidTest/kotlin**。
- 创建一个名为
ItemDaoTest.kt
的 Kotlin 类。 - 使用
@RunWith(AndroidJUnit4::class)
注解ItemDaoTest
类。您的类现在看起来类似于以下示例代码
package com.example.inventory
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ItemDaoTest {
}
- 在类内部,添加类型为
ItemDao
和InventoryDatabase
的私有var
变量。
import com.example.inventory.data.InventoryDatabase
import com.example.inventory.data.ItemDao
private lateinit var itemDao: ItemDao
private lateinit var inventoryDatabase: InventoryDatabase
- 添加一个创建数据库的函数,并使用
@Before
进行注解,以便它可以在每个测试之前运行。 - 在方法内部,初始化
itemDao
。
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import org.junit.Before
@Before
fun createDb() {
val context: Context = ApplicationProvider.getApplicationContext()
// Using an in-memory database because the information stored here disappears when the
// process is killed.
inventoryDatabase = Room.inMemoryDatabaseBuilder(context, InventoryDatabase::class.java)
// Allowing main thread queries, just for testing.
.allowMainThreadQueries()
.build()
itemDao = inventoryDatabase.itemDao()
}
在此函数中,您使用的是内存数据库,不会将其持久化到磁盘上。为此,您使用 inMemoryDatabaseBuilder() 函数。这样做是因为不需要持久化信息,而是在进程被杀死时需要将其删除。您使用 .allowMainThreadQueries()
在主线程中运行 DAO 查询,仅用于测试。
- 添加另一个关闭数据库的函数。使用
@After
对其进行注解,以便在每个测试后关闭数据库并运行。
import org.junit.After
import java.io.IOException
@After
@Throws(IOException::class)
fun closeDb() {
inventoryDatabase.close()
}
- 在
ItemDaoTest
类中声明数据库使用的项目,如下面的代码示例所示
import com.example.inventory.data.Item
private var item1 = Item(1, "Apples", 10.0, 20)
private var item2 = Item(2, "Bananas", 15.0, 97)
- 添加实用程序函数,用于向数据库中添加一个项目,然后添加两个项目。稍后,您将在测试中使用这些函数。将它们标记为
suspend
,以便它们可以在协程中运行。
private suspend fun addOneItemToDb() {
itemDao.insert(item1)
}
private suspend fun addTwoItemsToDb() {
itemDao.insert(item1)
itemDao.insert(item2)
}
- 编写一个测试,用于将单个项目插入数据库
insert()
。将测试命名为daoInsert_insertsItemIntoDB
,并使用@Test
进行注解。
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
@Test
@Throws(Exception::class)
fun daoInsert_insertsItemIntoDB() = runBlocking {
addOneItemToDb()
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], item1)
}
在此测试中,您使用实用程序函数 addOneItemToDb()
将一个项目添加到数据库。然后,您读取数据库中的第一个项目。使用 assertEquals()
,您将预期值与实际值进行比较。您使用 runBlocking{}
在一个新的协程中运行测试。此设置是您将实用程序函数标记为 suspend
的原因。
- 运行测试并确保其通过。
- 编写另一个测试,用于从数据库中获取
getAllItems()
。将测试命名为daoGetAllItems_returnsAllItemsFromDB
。
@Test
@Throws(Exception::class)
fun daoGetAllItems_returnsAllItemsFromDB() = runBlocking {
addTwoItemsToDb()
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], item1)
assertEquals(allItems[1], item2)
}
在上述测试中,您在协程内向数据库添加两个项目。然后,您读取这两个项目,并将它们与预期值进行比较。
6. 显示项目详细信息
在此任务中,您将在**项目详细信息**屏幕上读取和显示实体详细信息。您将使用库存应用数据库中的项目 UI 状态(例如名称、价格和数量),并使用 ItemDetailsScreen
可组合函数在**项目详细信息**屏幕上显示它们。 ItemDetailsScreen
可组合函数已为您预先编写,其中包含三个显示项目详细信息的 Text 可组合函数。
ui/item/ItemDetailsScreen.kt
此屏幕是启动代码的一部分,显示项目的详细信息,您将在以后的 codelab 中看到。在此 codelab 中,您不会处理此屏幕。 ItemDetailsViewModel.kt
是此屏幕对应的 ViewModel
。
- 在
HomeScreen
可组合函数中,请注意HomeBody()
函数调用。navigateToItemUpdate
正在传递给onItemClick
参数,当您单击列表中的任何项目时,该参数会被调用。
// No need to copy over
HomeBody(
itemList = homeUiState.itemList,
onItemClick = navigateToItemUpdate,
modifier = modifier
.padding(innerPadding)
.fillMaxSize()
)
- 打开
ui/navigation/InventoryNavGraph.kt
并注意HomeScreen
可组合函数中的navigateToItemUpdate
参数。此参数将导航目标指定为项目详细信息屏幕。
// No need to copy over
HomeScreen(
navigateToItemEntry = { navController.navigate(ItemEntryDestination.route) },
navigateToItemUpdate = {
navController.navigate("${ItemDetailsDestination.route}/${it}")
}
此部分的 onItemClick
功能已为您实现。当您单击列表项时,应用会导航到项目详细信息屏幕。
- 单击库存列表中的任何项目以查看项目详细信息屏幕,其中字段为空。
要使用项目详细信息填充文本字段,您需要在 ItemDetailsScreen()
中收集 UI 状态。
- 在
UI/Item/ItemDetailsScreen.kt
中,向ItemDetailsScreen
可组合函数添加一个新的参数,类型为ItemDetailsViewModel
,并使用工厂方法对其进行初始化。
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.inventory.ui.AppViewModelProvider
@Composable
fun ItemDetailsScreen(
navigateToEditItem: (Int) -> Unit,
navigateBack: () -> Unit,
modifier: Modifier = Modifier,
viewModel: ItemDetailsViewModel = viewModel(factory = AppViewModelProvider.Factory)
)
- 在
ItemDetailsScreen()
可组合函数内部,创建一个名为uiState
的val
用于收集 UI 状态。使用collectAsState()
收集uiState
StateFlow
并通过State
表示其最新值。Android Studio 显示未解析的引用错误。
import androidx.compose.runtime.collectAsState
val uiState = viewModel.uiState.collectAsState()
- 要解决此错误,请在
ItemDetailsViewModel
类中创建一个名为uiState
的val
,类型为StateFlow<ItemDetailsUiState>
。 - 从项目存储库中检索数据,并使用扩展函数
toItemDetails()
将其映射到ItemDetailsUiState
。扩展函数Item.toItemDetails()
已作为启动代码的一部分为您编写。
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
val uiState: StateFlow<ItemDetailsUiState> =
itemsRepository.getItemStream(itemId)
.filterNotNull()
.map {
ItemDetailsUiState(itemDetails = it.toItemDetails())
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
initialValue = ItemDetailsUiState()
)
- 将
ItemsRepository
传递给ItemDetailsViewModel
以解决Unresolved reference: itemsRepository
错误。
class ItemDetailsViewModel(
savedStateHandle: SavedStateHandle,
private val itemsRepository: ItemsRepository
) : ViewModel() {
- 在
ui/AppViewModelProvider.kt
中,更新ItemDetailsViewModel
的初始化程序,如下面的代码片段所示
initializer {
ItemDetailsViewModel(
this.createSavedStateHandle(),
inventoryApplication().container.itemsRepository
)
}
- 返回
ItemDetailsScreen.kt
,并注意ItemDetailsScreen()
可组合函数中的错误已解决。 - 在
ItemDetailsScreen()
可组合函数中,更新ItemDetailsBody()
函数调用,并将uiState.value
传递给itemUiState
参数。
ItemDetailsBody(
itemUiState = uiState.value,
onSellItem = { },
onDelete = { },
modifier = modifier.padding(innerPadding)
)
- 观察
ItemDetailsBody()
和ItemInputForm()
的实现。您正在将从ItemDetailsBody()
中选择的当前item
传递给ItemDetails()
。
// No need to copy over
@Composable
private fun ItemDetailsBody(
itemUiState: ItemUiState,
onSellItem: () -> Unit,
onDelete: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
//...
) {
var deleteConfirmationRequired by rememberSaveable { mutableStateOf(false) }
ItemDetails(
item = itemDetailsUiState.itemDetails.toItem(), modifier = Modifier.fillMaxWidth()
)
//...
}
- 运行应用。当您单击**库存**屏幕上的任何列表元素时,将显示**项目详细信息**屏幕。
- 注意,屏幕不再为空白。它显示从库存数据库检索到的实体详细信息。
- 点击**出售**按钮。没有任何反应!
在下一部分中,您将实现**出售**按钮的功能。
7. 实现项目详细信息屏幕
ui/item/ItemEditScreen.kt
项目编辑屏幕已作为启动代码的一部分提供给您。
此布局包含用于编辑任何新库存项目详细信息的文本字段可组合函数。
此应用的代码仍未完全发挥功能。例如,在**项目详细信息**屏幕上,当您点击**出售**按钮时,**库存数量**不会减少。当您点击**删除**按钮时,应用会提示您确认对话框。但是,当您选择**是**按钮时,应用实际上并没有删除该项目。
最后,FAB 按钮 会打开一个空的**编辑项目**屏幕。
在本节中,您将实现**出售**、**删除**和 FAB 按钮的功能。
8. 实现出售项目
在本节中,您将扩展应用的功能以实现出售功能。此更新涉及以下任务
- 为 DAO 函数添加一个测试,用于更新实体。
- 在
ItemDetailsViewModel
中添加一个函数,以减少数量并在应用数据库中更新实体。 - 如果数量为零,则禁用**出售**按钮。
- 在
ItemDaoTest.kt
中,添加一个名为daoUpdateItems_updatesItemsInDB()
的函数,不带参数。使用@Test
和@Throws(Exception::class)
进行注解。
@Test
@Throws(Exception::class)
fun daoUpdateItems_updatesItemsInDB()
- 定义函数并创建一个
runBlocking
块。在其中调用addTwoItemsToDb()
。
fun daoUpdateItems_updatesItemsInDB() = runBlocking {
addTwoItemsToDb()
}
- 使用不同的值更新这两个实体,调用
itemDao.update
。
itemDao.update(Item(1, "Apples", 15.0, 25))
itemDao.update(Item(2, "Bananas", 5.0, 50))
- 使用
itemDao.getAllItems()
检索实体。将它们与更新后的实体进行比较并断言。
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], Item(1, "Apples", 15.0, 25))
assertEquals(allItems[1], Item(2, "Bananas", 5.0, 50))
- 确保完成后的函数如下所示
@Test
@Throws(Exception::class)
fun daoUpdateItems_updatesItemsInDB() = runBlocking {
addTwoItemsToDb()
itemDao.update(Item(1, "Apples", 15.0, 25))
itemDao.update(Item(2, "Bananas", 5.0, 50))
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], Item(1, "Apples", 15.0, 25))
assertEquals(allItems[1], Item(2, "Bananas", 5.0, 50))
}
- 运行测试并确保其通过。
在 ViewModel
中添加一个函数
- 在
ItemDetailsViewModel.kt
中,在ItemDetailsViewModel
类内部,添加一个名为reduceQuantityByOne()
的函数,不带参数。
fun reduceQuantityByOne() {
}
- 在函数内部,使用
viewModelScope.launch{}
启动一个协程。
import kotlinx.coroutines.launch
import androidx.lifecycle.viewModelScope
viewModelScope.launch {
}
- 在
launch
块内部,创建一个名为currentItem
的val
,并将其设置为uiState.value.toItem()
.
val currentItem = uiState.value.toItem()
uiState.value
的类型为 ItemUiState
。您使用扩展函数 toItem
()
将其转换为 Item
实体类型。
- 添加一个
if
语句,以检查quality
是否大于0
。 - 在
itemsRepository
上调用updateItem()
并传入更新后的currentItem
。使用copy()
更新quantity
值,使函数看起来像下面这样
fun reduceQuantityByOne() {
viewModelScope.launch {
val currentItem = uiState.value.itemDetails.toItem()
if (currentItem.quantity > 0) {
itemsRepository.updateItem(currentItem.copy(quantity = currentItem.quantity - 1))
}
}
}
- 返回
ItemDetailsScreen.kt
。 - 在
ItemDetailsScreen
可组合函数中,找到ItemDetailsBody()
函数调用。 - 在
onSellItem
lambda 中,调用viewModel.reduceQuantityByOne()
。
ItemDetailsBody(
itemUiState = uiState.value,
onSellItem = { viewModel.reduceQuantityByOne() },
onDelete = { },
modifier = modifier.padding(innerPadding)
)
- 运行应用程序。
- 在“**库存**”屏幕上,点击一个列表元素。当“**商品详情**”屏幕显示时,点击“**出售**”,注意数量值减少了 1。
- 在“**商品详情**”屏幕上,持续点击“**出售**”按钮,直到数量为零。
数量达到零后,再次点击“**出售**”。没有视觉变化,因为函数 reduceQuantityByOne()
在更新数量之前会检查数量是否大于零。
为了给用户提供更好的反馈,您可能希望在没有商品可出售时禁用“**出售**”按钮。
- 在
ItemDetailsViewModel
类中,根据map
转换中的it
.quantity
设置outOfStock
值。
val uiState: StateFlow<ItemDetailsUiState> =
itemsRepository.getItemStream(itemId)
.filterNotNull()
.map {
ItemDetailsUiState(outOfStock = it.quantity <= 0, itemDetails = it.toItemDetails())
}.stateIn(
//...
)
- 运行您的应用程序。注意,当库存中的数量为零时,应用程序会禁用“**出售**”按钮。
恭喜您在应用程序中实现了“**出售**”商品功能!
删除商品实体
与之前的任务一样,您必须通过实现删除功能来进一步扩展应用程序的功能。此功能比出售功能更容易实现。该过程涉及以下任务
- 为删除 DAO 查询添加测试。
- 在
ItemDetailsViewModel
类中添加一个函数,用于从数据库中删除实体。 - 更新 ItemDetailsBody 可组合函数。
添加 DAO 测试
- 在
ItemDaoTest.kt
中,添加一个名为daoDeleteItems_deletesAllItemsFromDB()
的测试。
@Test
@Throws(Exception::class)
fun daoDeleteItems_deletesAllItemsFromDB()
- 使用 runBlocking {} 启动一个协程。
fun daoDeleteItems_deletesAllItemsFromDB() = runBlocking {
}
- 向数据库中添加两个商品,并对这两个商品调用
itemDao.delete()
以将其从数据库中删除。
addTwoItemsToDb()
itemDao.delete(item1)
itemDao.delete(item2)
- 从数据库中检索实体,并检查列表是否为空。完整的测试应如下所示
import org.junit.Assert.assertTrue
@Test
@Throws(Exception::class)
fun daoDeleteItems_deletesAllItemsFromDB() = runBlocking {
addTwoItemsToDb()
itemDao.delete(item1)
itemDao.delete(item2)
val allItems = itemDao.getAllItems().first()
assertTrue(allItems.isEmpty())
}
在 ItemDetailsViewModel
中添加删除函数
- 在
ItemDetailsViewModel
中,添加一个名为deleteItem()
的新函数,该函数不带参数且不返回值。 - 在
deleteItem()
函数内部,添加itemsRepository.deleteItem()
函数调用,并传入uiState.value.
toItem
()
。
suspend fun deleteItem() {
itemsRepository.deleteItem(uiState.value.itemDetails.toItem())
}
在此函数中,您使用 toItem()
扩展函数将 uiState
从 itemDetails
类型转换为 Item
实体类型。
- 在
ui/item/ItemDetailsScreen
可组合函数中,添加一个名为coroutineScope
的val
并将其设置为rememberCoroutineScope()
。此方法返回一个绑定到其调用位置(ItemDetailsScreen
可组合函数)的协程作用域。
import androidx.compose.runtime.rememberCoroutineScope
val coroutineScope = rememberCoroutineScope()
- 滚动到
ItemDetailsBody()
函数。 - 在
onDelete
lambda 内部使用coroutineScope
启动一个协程。 - 在
launch
块内部,在viewModel
上调用deleteItem()
方法。
import kotlinx.coroutines.launch
ItemDetailsBody(
itemUiState = uiState.value,
onSellItem = { viewModel.reduceQuantityByOne() },
onDelete = {
coroutineScope.launch {
viewModel.deleteItem()
}
modifier = modifier.padding(innerPadding)
)
- 删除商品后,导航回库存屏幕。
- 在
deleteItem()
函数调用后调用navigateBack()
。
onDelete = {
coroutineScope.launch {
viewModel.deleteItem()
navigateBack()
}
- 仍在
ItemDetailsScreen.kt
文件中,滚动到ItemDetailsBody()
函数。
此函数是启动代码的一部分。此可组合函数显示一个警报对话框以获取用户在删除商品之前的确认,并在点击“是”时调用 deleteItem()
函数。
// No need to copy over
@Composable
private fun ItemDetailsBody(
itemUiState: ItemUiState,
onSellItem: () -> Unit,
onDelete: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
/*...*/
) {
//...
if (deleteConfirmationRequired) {
DeleteConfirmationDialog(
onDeleteConfirm = {
deleteConfirmationRequired = false
onDelete()
},
//...
)
}
}
}
点击“否”时,应用程序会关闭警报对话框。showConfirmationDialog()
函数显示以下警报
- 运行应用程序。
- 在“**库存**”屏幕上选择一个列表元素。
- 在“**商品详情**”屏幕上,点击“**删除**”。
- 在警报对话框中点击“是”,应用程序会导航回“**库存**”屏幕。
- 确认您删除的实体不再存在于应用程序数据库中。
恭喜您实现了删除功能!
编辑商品实体
与前面的部分类似,在本部分中,您向应用程序添加了另一个功能增强功能,用于编辑商品实体。
以下是编辑应用程序数据库中实体的步骤概述
- 向测试获取商品 DAO 查询添加测试。
- 使用实体详细信息填充“**编辑商品**”屏幕中的文本字段。
- 使用 Room 更新数据库中的实体。
添加 DAO 测试
- 在
ItemDaoTest.kt
中,添加一个名为daoGetItem_returnsItemFromDB()
的测试。
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB()
- 定义函数。在协程内部,向数据库中添加一个商品。
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB() = runBlocking {
addOneItemToDb()
}
- 使用
itemDao.getItem()
函数从数据库中检索实体,并将其设置为名为item
的val
。
val item = itemDao.getItem(1)
- 将实际值与检索到的值进行比较,并使用
assertEquals()
断言。您的完整测试如下所示
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB() = runBlocking {
addOneItemToDb()
val item = itemDao.getItem(1)
assertEquals(item.first(), item1)
}
- 运行测试并确保其通过。
填充文本字段
如果运行应用程序,转到“**商品详情**”屏幕,然后点击悬浮操作按钮,您会发现屏幕的标题现在是“**编辑商品**”。但是,所有文本字段都是空的。在此步骤中,您将使用实体详细信息填充“**编辑商品**”屏幕中的文本字段。
- 在
ItemDetailsScreen.kt
中,滚动到ItemDetailsScreen
可组合函数。 - 在
FloatingActionButton()
中,将onClick
参数更改为包含uiState.value.itemDetails.id
,这是所选实体的id
。您使用此id
来检索实体详细信息。
FloatingActionButton(
onClick = { navigateToEditItem(uiState.value.itemDetails.id) },
modifier = /*...*/
)
- 在
ItemEditViewModel
类中,添加一个init
块。
init {
}
- 在
init
块内部,使用 viewModelScope.
launch
启动一个协程。
import kotlinx.coroutines.launch
viewModelScope.launch { }
- 在
launch
块内部,使用itemsRepository.getItemStream(itemId)
检索实体详细信息。
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
init {
viewModelScope.launch {
itemUiState = itemsRepository.getItemStream(itemId)
.filterNotNull()
.first()
.toItemUiState(true)
}
}
在此 launch 块中,您添加了一个过滤器以返回仅包含非空值的流。使用 toItemUiState()
,您可以将 item
实体转换为 ItemUiState
。您将 actionEnabled
值作为 true
传递以启用“**保存**”按钮。
要解决 Unresolved reference: itemsRepository
错误,您需要将 ItemsRepository 作为依赖项传递给视图模型。
- 向
ItemEditViewModel
类添加一个构造函数参数。
class ItemEditViewModel(
savedStateHandle: SavedStateHandle,
private val itemsRepository: ItemsRepository
)
- 在
AppViewModelProvider.kt
文件中,在ItemEditViewModel
初始化器中,将ItemsRepository
对象作为参数添加。
initializer {
ItemEditViewModel(
this.createSavedStateHandle(),
inventoryApplication().container.itemsRepository
)
}
- 运行应用程序。
- 转到“**商品详情**”并点击 悬浮操作按钮。
- 注意,字段会填充商品详细信息。
- 编辑库存数量或任何其他字段,然后点击“**保存**”。
什么也没发生!这是因为您没有更新应用程序数据库中的实体。您将在下一节中修复此问题。
使用 Room 更新实体
在此最终任务中,您添加了实现更新功能的代码的最终部分。您在 ViewModel 中定义必要的函数,并在 ItemEditScreen
中使用它们。
是时候再次编码了!
- 在
ItemEditViewModel
类中,添加一个名为updateUiState()
的函数,该函数接受一个ItemUiState
对象且不返回值。此函数使用用户输入的新值更新itemUiState
。
fun updateUiState(itemDetails: ItemDetails) {
itemUiState =
ItemUiState(itemDetails = itemDetails, isEntryValid = validateInput(itemDetails))
}
在此函数中,您将传入的 itemDetails
分配给 itemUiState
并更新 isEntryValid
值。如果 itemDetails
为 true
,则应用程序会启用“**保存**”按钮。仅当用户输入的输入有效时,您才将其值设置为 true
。
- 转到
ItemEditScreen.kt
文件。 - 在
ItemEditScreen
可组合函数中,向下滚动到ItemEntryBody()
函数调用。 - 将
onItemValueChange
参数值设置为新函数updateUiState
。
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = { },
modifier = modifier.padding(innerPadding)
)
- 运行应用程序。
- 转到“**编辑商品**”屏幕。
- 使其中一个实体值为空,使其无效。注意“**保存**”按钮是如何自动禁用的。
- 返回
ItemEditViewModel
类并添加一个名为updateItem()
的suspend
函数,该函数不带参数。您使用此函数将更新的实体保存到 Room 数据库。
suspend fun updateItem() {
}
- 在
getUpdatedItemEntry()
函数内部,添加一个if
条件以使用函数validateInput()
验证用户输入。 - 在
itemsRepository
上调用updateItem()
函数,传入itemUiState.itemDetails.
toItem
()
。可以添加到 Room 数据库的实体需要是Item
类型。完整的函数如下所示
suspend fun updateItem() {
if (validateInput(itemUiState.itemDetails)) {
itemsRepository.updateItem(itemUiState.itemDetails.toItem())
}
}
- 返回
ItemEditScreen
可组合函数。您需要一个协程作用域来调用updateItem()
函数。创建一个名为coroutineScope
的val
并将其设置为rememberCoroutineScope()
。
import androidx.compose.runtime.rememberCoroutineScope
val coroutineScope = rememberCoroutineScope()
- 在 ItemEntryBody() 函数调用中,更新
onSaveClick
函数参数以在coroutineScope
中启动一个协程。 - 在
launch
代码块内,调用viewModel
上的updateItem()
函数并返回。
import kotlinx.coroutines.launch
onSaveClick = {
coroutineScope.launch {
viewModel.updateItem()
navigateBack()
}
},
完成后的 ItemEntryBody()
函数调用如下所示
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = {
coroutineScope.launch {
viewModel.updateItem()
navigateBack()
}
},
modifier = modifier.padding(innerPadding)
)
- 运行应用程序并尝试编辑库存项目。您现在可以编辑库存应用程序数据库中的任何项目。
恭喜您创建了第一个使用 Room 管理数据库的应用程序!
9. 解决方案代码
此代码实验室的解决方案代码位于 GitHub 存储库和下面显示的分支中
10. 了解更多
Android 开发者文档
Kotlin 参考