一、实验名称
使用Room在Inventory应用中实现数据的持久化。
二、参考资料
《Android开发者官方网站:Android 移动应用开发者工具 – Android 开发者 | Android Developers》、第七章课件。
三、实验目的
练习在 Android 应用中使用Room实现数据的增删改查操作。
四、实验内容
参考第七章的课件,在Inventory应用中使用Room实现库存商品信息的列表、入库、修改、删除、显示详情功能。
实验报告
一、程序代码
1. 添加Room依赖项,build.gradle.kts(Module: InventoryApp.app)
// Testing
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
//ROOM
implementation("androidx.room:room-runtime:rootProject.extra["roomversion"]")ksp("androidx.room:room−compiler:{rootProject.extra["room_version"]}")
implementation("androidx.room:room-ktx:${rootProject.extra["room_version"]}")
2. src\androidTest\kotlin\ItemDaoTest类
package com.example.inventory
@RunWith(AndroidJUnit4::class)
class ItemDaoTest {
private lateinit var itemDao: ItemDao
private lateinit var inventoryDatabase: InventoryDatabase
private var item1=Item(1,"Apples",10.0,20)
private var item2=Item(2,"Bananas",15.0,97)
@Before
fun createDb() {
val context: Context = ApplicationProvider.getApplicationContext()
inventoryDatabase = Room.inMemoryDatabaseBuilder(context, InventoryDatabase::class.java)
.allowMainThreadQueries()
.build()
itemDao = inventoryDatabase.itemDao()
}
@After
@Throws(IOException::class)
fun closeDb() {
inventoryDatabase.close()
}
private suspend fun addOneItemToDb() {
itemDao.insert(item1)
}
private suspend fun addTwoItemsToDb() {
itemDao.insert(item1)
itemDao.insert(item2)
}
@Test
@Throws(Exception::class)
fun daoInsert_insertsItemIntoDB() = runBlocking {
addOneItemToDb()
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], item1)
}
@Test
@Throws(Exception::class)
fun daoGetAllItems_returnsAllItemsFromDB() = runBlocking {
addTwoItemsToDb()
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], item1)
assertEquals(allItems[1], item2)
}
@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))
}
@Test
@Throws(Exception::class)
fun daoDeleteItems_deletesAllItemsFromDB() = runBlocking {
addTwoItemsToDb()
itemDao.delete(item1)
itemDao.delete(item2)
val allItems = itemDao.getAllItems().first()
assertTrue(allItems.isEmpty())
}
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB() = runBlocking {
addOneItemToDb()
val item = itemDao.getItem(1)
assertEquals(item.first(), item1)
}
}
3. 在项目的data子包中创建一个Item实体类
package com.example.inventory.data
/创建一个Item实体类,并定义字段来存储每个商品的以下商品目录信息
@Entity(tableName = "items")
data class Item(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,://id用于存储主键
val name: String,//name用于存储商品名称
val price: Double,//price用于存储商品价格
val quantity: Int//quantity用于存储库存数量
)
4. 在data软件包中,创建Kotlin接口ItemDao,在该接口中通过DAO的注解,实现数据库的增删改查操作
package com.example.inventory.data
@Dao
interface ItemDao {
@Query("SELECT * from items ORDER BY name ASC")
fun getAllItems(): Flow<List<Item>>
@Query("SELECT * from items WHERE id = :id")
fun getItem(id: Int): Flow<Item>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)
@Update
suspend fun update(item: Item)
@Delete
suspend fun delete(item: Item)
}
5. 在data 软件包中,创建一个Kotlin类InventoryDatabase的数据库实例
package com.example.inventory.data
@Database(entities = [Item :: class], version = 1, exportSchema = false)
abstract class InventoryDatabase() : RoomDatabase(), Parcelable {
abstract fun itemDao(): ItemDao
companion object {
@Volatile
private var Instance: InventoryDatabase? = null
fun grtDatabase(context: Context):InventoryDatabase {
return Instance ?: synchronized(this) {
Room.databaseBuilder(context,InventoryDatabase::class.java,"item_database")
.fallbackToDestructiveMigration()
.build()
.also { Instance = it }
}
}
}
}
6. 实现仓储模式实现ItemsRepository 接口和OfflineltemsRepository类,该类通过DAO来操作数据。
(1)ItemsRepository接口的代码如下所示:
package com.example.inventory.data
interface ItemsRepository {
fun getAllItemsStream(): Flow<List<Item>>
fun getItemStream(id: Int): Flow<Item?>
suspend fun insertItem(item: Item)
suspend fun deleteItem(item: Item)
suspend fun updateItem(item: Item)
}
(2)OfflineItemsRepository 类的代码如下所示:
package com.example.inventory.data
class OfflineItemsRepository(private val itemDao: ItemDao) : ItemsRepository {
override fun getAllItemsStream(): Flow<List<Item>> = itemDao.getAllItems()
override fun getItemStream(id: Int): Flow<Item?> = itemDao.getItem(id)
override suspend fun insertItem(item: Item) = itemDao.insert(item)
override suspend fun deleteItem(item: Item) = itemDao.delete(item)
override suspend fun updateItem(item: Item) = itemDao.update(item)
}
(3)在data软件包下新建AppContainer.kt文件:
package com.example.inventory.data
interface AppContainer {
val itemsRepository: ItemsRepository
}
class AppDataContainer(private val context: Context) : AppContainer {
override val itemsRepository: ItemsRepository by lazy {
OfflineItemsRepository(InventoryDatabase.grtDatabase(context).itemDao())
}
}
(4)在项目的顶层软件包下新建InventoryApplication类:
package com.example.inventory
import android.app.Application
class InventoryApplication : Application() {
lateinit var container: AppContainer
override fun onCreate() {
super.onCreate()
container = AppDataContainer(this)
}
}
6. ui.item/ItemEntryViewModel.kt
package com.example.inventory.ui.item
class ItemEntryViewModel(private val itemsRepository: ItemsRepository) : ViewModel() {
var itemUiState by mutableStateOf(ItemUiState())
private set
fun updateUiState(itemDetails: ItemDetails) {
itemUiState =
ItemUiState(itemDetails = itemDetails, isEntryValid = validateInput(itemDetails))
}
private fun validateInput(uiState: ItemDetails = itemUiState.itemDetails): Boolean {
return with(uiState) {
name.isNotBlank() && price.isNotBlank() && quantity.isNotBlank()
}
}
suspend fun updateItem() {
if (validateInput()){
itemsRepository.insertItem(itemUiState.itemDetails.toItem())
}
}
}
data class ItemUiState(
val itemDetails: ItemDetails = ItemDetails(),
val isEntryValid: Boolean = false
)
data class ItemDetails(
val id: Int = 0,
val name: String = "",
val price: String = "",
val quantity: String = "",
)
fun ItemDetails.toItem(): Item = Item(
id = id,
name = name,
price = price.toDoubleOrNull() ?: 0.0,
quantity = quantity.toIntOrNull() ?: 0
)
fun Item.formatedPrice(): String {
return NumberFormat.getCurrencyInstance().format(price)
}
fun Item.toItemUiState(isEntryValid: Boolean = false): ItemUiState = ItemUiState(
itemDetails = this.toItemDetails(),
isEntryValid = isEntryValid
)
fun Item.toItemDetails(): ItemDetails = ItemDetails(
id = id,
name = name,
price = price.toString(),
quantity = quantity.toString()
)
7. ui/AppViewModelProvider.kt
package com.example.inventory.ui
import android.app.Application
object AppViewModelProvider {
val Factory = viewModelFactory {
// Initializer for ItemEditViewModel
initializer {
ItemEditViewModel(
this.createSavedStateHandle(),
inventoryApplication().container.itemsRepository
)
}
// Other Initializers
// Initializer for ItemEntryViewModel
initializer {
ItemEntryViewModel(inventoryApplication().container.itemsRepository)
}
// Initializer for ItemDetailsViewModel
initializer {
ItemDetailsViewModel(
this.createSavedStateHandle(),
inventoryApplication().container.itemsRepository
)
}
// Initializer for HomeViewModel
initializer {
HomeViewModel()
}
}
}
fun CreationExtras.inventoryApplication(): InventoryApplication =
(this[AndroidViewModelFactory.APPLICATION_KEY] as InventoryApplication)
8. ItemEntryScreen.kt
package com.example.inventory.ui.item
object ItemEntryDestination : NavigationDestination {
override val route = "item_entry"
override val titleRes = R.string.item_entry_title
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ItemEntryScreen(
navigateBack: () -> Unit,
onNavigateUp: () -> Unit,
canNavigateBack: Boolean = true,
viewModel: ItemEntryViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
//需要使用协程作用域来调用updateItem()函数,创建一个名为coroutineScope的变量并将其设置为rememberCoroutineScope()
val coroutineScope= rememberCoroutineScope()
Scaffold(
topBar = {
InventoryTopAppBar(
title = stringResource(ItemEntryDestination.titleRes),
canNavigateBack = canNavigateBack,
navigateUp = onNavigateUp
)
}
) { innerPadding ->
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState, //将onItemValueChange参数值设为新函数updateUiState
onSaveClick = {
coroutineScope.launch { //在coroutineScope中启动协程
viewModel.updateItem() //在launch代码块内,对viewModel 调用updateItem()
navigateBack()
}
},
modifier = Modifier
.padding(innerPadding)
.verticalScroll(rememberScrollState())
.fillMaxWidth()
)
}
}
@Composable
fun ItemEntryBody(
itemUiState: ItemUiState,
onItemValueChange: (ItemDetails) -> Unit,
onSaveClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.padding_large)),
modifier = modifier.padding(dimensionResource(id = R.dimen.padding_medium))
) {
ItemInputForm(
itemDetails = itemUiState.itemDetails,
onValueChange = onItemValueChange,
modifier = Modifier.fillMaxWidth()
)
Button(
onClick = onSaveClick,
enabled = itemUiState.isEntryValid,
shape = MaterialTheme.shapes.small,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.save_action))
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ItemInputForm(
itemDetails: ItemDetails,
modifier: Modifier = Modifier,
onValueChange: (ItemDetails) -> Unit = {},
enabled: Boolean = true
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.padding_medium))
) {
OutlinedTextField(
value = itemDetails.name,
onValueChange = { onValueChange(itemDetails.copy(name = it)) },
label = { Text(stringResource(R.string.item_name_req)) },
colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.secondaryContainer,
unfocusedContainerColor = MaterialTheme.colorScheme.secondaryContainer,
disabledContainerColor = MaterialTheme.colorScheme.secondaryContainer,
),
modifier = Modifier.fillMaxWidth(),
enabled = enabled,
singleLine = true
)
OutlinedTextField(
value = itemDetails.price,
onValueChange = { onValueChange(itemDetails.copy(price = it)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
label = { Text(stringResource(R.string.item_price_req)) },
colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.secondaryContainer,
unfocusedContainerColor = MaterialTheme.colorScheme.secondaryContainer,
disabledContainerColor = MaterialTheme.colorScheme.secondaryContainer,
),
leadingIcon = { Text(Currency.getInstance(Locale.getDefault()).symbol) },
modifier = Modifier.fillMaxWidth(),
enabled = enabled,
singleLine = true
)
OutlinedTextField(
value = itemDetails.quantity,
onValueChange = { onValueChange(itemDetails.copy(quantity = it)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
label = { Text(stringResource(R.string.quantity_req)) },
colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.secondaryContainer,
unfocusedContainerColor = MaterialTheme.colorScheme.secondaryContainer,
disabledContainerColor = MaterialTheme.colorScheme.secondaryContainer,
),
modifier = Modifier.fillMaxWidth(),
enabled = enabled,
singleLine = true
)
if (enabled) {
Text(
text = stringResource(R.string.required_fields),
modifier = Modifier.padding(start = dimensionResource(id = R.dimen.padding_medium))
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun ItemEntryScreenPreview() {
InventoryTheme {
ItemEntryBody(itemUiState = ItemUiState(
ItemDetails(
name = "Item name", price = "10.00", quantity = "5"
)
), onItemValueChange = {}, onSaveClick = {})
}
}
9. ItemEditViewModel.kt
package com.example.inventory.ui.item
class ItemEditViewModel(
savedStateHandle: SavedStateHandle,
private val itemsRepository: ItemsRepository
) : ViewModel() {
var itemUiState by mutableStateOf(ItemUiState())
private set
private val itemId: Int = checkNotNull(savedStateHandle[ItemEditDestination.itemIdArg])
private fun validateInput(uiState: ItemDetails = itemUiState.itemDetails): Boolean {
return with(uiState) {
name.isNotBlank() && price.isNotBlank() && quantity.isNotBlank()
}
}
init {
viewModelScope.launch {
itemUiState = itemsRepository.getItemStream(itemId)
.filterNotNull()
.first()
.toItemUiState(true)
}
}
fun updateUiState(itemDetails: ItemDetails) {
itemUiState =
ItemUiState(itemDetails = itemDetails, isEntryValid = validateInput(itemDetails))
}
suspend fun updateItem() {
if (validateInput(itemUiState.itemDetails)) {
itemsRepository.updateItem(itemUiState.itemDetails.toItem())
}
}
}
10. ItemEditScreen.kt
package com.example.inventory.ui.item
object ItemEditDestination : NavigationDestination {
override val route = "item_edit"
override val titleRes = R.string.edit_item_title
const val itemIdArg = "itemId"
val routeWithArgs = "route/{itemIdArg}"
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ItemEditScreen(
navigateBack: () -> Unit,
onNavigateUp: () -> Unit,
modifier: Modifier = Modifier,
viewModel: ItemEditViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
Scaffold(
topBar = {
InventoryTopAppBar(
title = stringResource(ItemEditDestination.titleRes),
canNavigateBack = true,
navigateUp = onNavigateUp
)
},
modifier = modifier
) { innerPadding ->
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = {},
modifier = Modifier.padding(innerPadding)
)
}
}
@Preview(showBackground = true)
@Composable
fun ItemEditScreenPreview() {
InventoryTheme {
ItemEditScreen(navigateBack = { /*Do nothing*/ }, onNavigateUp = { /*Do nothing*/ })
}
}
11. ItemDetailsViewModel.kt
package com.example.inventory.ui.item
class ItemDetailsViewModel(
savedStateHandle: SavedStateHandle,
private val itemsRepository: ItemsRepository
) : ViewModel() {
private val itemId: Int = checkNotNull(savedStateHandle[ItemDetailsDestination.itemIdArg])
val uiState: StateFlow<ItemDetailsUiState> =
itemsRepository.getItemStream(itemId)
.filterNotNull()
.map {
ItemDetailsUiState(outOfStock = it.quantity <= 0,itemDetails = it.toItemDetails())
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
initialValue = ItemDetailsUiState()
)
companion object {
private const val TIMEOUT_MILLIS = 5_000L
}
fun reduceQuantityByOne() {
viewModelScope.launch {
val currentItem = uiState.value.itemDetails.toItem()
if (currentItem.quantity > 0) {
itemsRepository.updateItem(currentItem.copy(quantity = currentItem.quantity - 1))
}
}
}
suspend fun deleteItem() {
itemsRepository.deleteItem(uiState.value.itemDetails.toItem())
}
}
data class ItemDetailsUiState(
val outOfStock: Boolean = true,
val itemDetails: ItemDetails = ItemDetails()
)
12. ItemDetailsScreen.kt
package com.example.inventory.ui.item
object ItemDetailsDestination : NavigationDestination {
override val route = "item_details"
override val titleRes = R.string.item_detail_title
const val itemIdArg = "itemId"
val routeWithArgs = "route/{itemIdArg}"
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ItemDetailsScreen(
navigateToEditItem: (Int) -> Unit,
navigateBack: () -> Unit,
modifier: Modifier = Modifier,
viewModel: ItemDetailsViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val uiState = viewModel.uiState.collectAsState()
val coroutineScope = rememberCoroutineScope()
Scaffold(
topBar = {
InventoryTopAppBar(
title = stringResource(ItemDetailsDestination.titleRes),
canNavigateBack = true,
navigateUp = navigateBack
)
}, floatingActionButton = {
FloatingActionButton(
onClick = { navigateToEditItem(uiState.value.itemDetails.id) },
shape = MaterialTheme.shapes.medium,
modifier = Modifier.padding(dimensionResource(id = R.dimen.padding_large))
) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = stringResource(R.string.edit_item_title),
)
}
}, modifier = modifier
) { innerPadding ->
ItemDetailsBody(
itemDetailsUiState = uiState.value,
onSellItem = { viewModel.reduceQuantityByOne() },
onDelete = {
coroutineScope.launch {
viewModel.deleteItem()
}
},
modifier = Modifier
.padding(innerPadding)
)
}
}
@Composable
private fun ItemDetailsBody(
itemDetailsUiState: ItemDetailsUiState,
onSellItem: () -> Unit,
onDelete: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier.padding(dimensionResource(id = R.dimen.padding_medium)),
verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.padding_medium))
) {
var deleteConfirmationRequired by rememberSaveable { mutableStateOf(false) }
ItemDetails(
item = itemDetailsUiState.itemDetails.toItem(),
modifier = Modifier.fillMaxWidth()
)
Button(
onClick = onSellItem,
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.small,
enabled = true
) {
Text(stringResource(R.string.sell))
}
OutlinedButton(
onClick = { deleteConfirmationRequired = true },
shape = MaterialTheme.shapes.small,
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.delete))
}
if (deleteConfirmationRequired) {
DeleteConfirmationDialog(
onDeleteConfirm = {
deleteConfirmationRequired = false
onDelete()
},
onDeleteCancel = { deleteConfirmationRequired = false },
modifier = Modifier.padding(dimensionResource(id = R.dimen.padding_medium))
)
}
}
}
@Composable
fun ItemDetails(
item: Item, modifier: Modifier = Modifier
) {
Card(
modifier = modifier,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(id = R.dimen.padding_medium)),
verticalArrangement = Arrangement.spacedBy(
dimensionResource(id = R.dimen.padding_medium)
)
) {
ItemDetailsRow(
labelResID = R.string.item,
itemDetail = item.name,
modifier = Modifier.padding(
horizontal = dimensionResource(id = R.dimen.padding_medium)
)
)
ItemDetailsRow(
labelResID = R.string.quantity_in_stock,
itemDetail = item.quantity.toString(),
modifier = Modifier.padding(
horizontal = dimensionResource(id = R.dimen.padding_medium)
)
)
ItemDetailsRow(
labelResID = R.string.price,
itemDetail = item.formatedPrice(),
modifier = Modifier.padding(
horizontal = dimensionResource(id = R.dimen.padding_medium)
)
)
}
}
}
@Composable
private fun ItemDetailsRow(
@StringRes labelResID: Int, itemDetail: String, modifier: Modifier = Modifier
) {
Row(modifier = modifier) {
Text(stringResource(labelResID))
Spacer(modifier = Modifier.weight(1f))
Text(text = itemDetail, fontWeight = FontWeight.Bold)
}
}
@Composable
private fun DeleteConfirmationDialog(
onDeleteConfirm: () -> Unit,
onDeleteCancel: () -> Unit,
modifier: Modifier = Modifier
) {
AlertDialog(onDismissRequest = { /* Do nothing */ },
title = { Text(stringResource(R.string.attention)) },
text = { Text(stringResource(R.string.delete_question)) },
modifier = modifier,
dismissButton = {
TextButton(onClick = onDeleteCancel) {
Text(stringResource(R.string.no))
}
},
confirmButton = {
TextButton(onClick = onDeleteConfirm) {
Text(stringResource(R.string.yes))
}
})
}
@Preview(showBackground = true)
@Composable
fun ItemDetailsScreenPreview() {
InventoryTheme {
ItemDetailsBody(
ItemDetailsUiState(
outOfStock = true,
itemDetails = ItemDetails(1, "Pen", "$100", "10")
),
onSellItem = {},
onDelete = {}
)
}
}
13. 更新界面状态ui./home/HomeViewModel.kt
package com.example.inventory.ui.home
class HomeViewModel(itemsRepository: ItemsRepository) : ViewModel() {
val homeUiState: StateFlow<HomeUiState> =
itemsRepository.getAllItemsStream()
.map { HomeUiState(it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
initialValue = HomeUiState()
)
companion object {
private const val TIMEOUT_MILLIS = 5_000L
}
}
data class HomeUiState(val itemList: List<Item> = listOf())
14. 显示商品目录数据HomeScreen.kt
package com.example.inventory.ui.home
object HomeDestination : NavigationDestination {
override val route = "home"
override val titleRes = R.string.app_name
}
@OptIn(ExperimentalMaterial3Api::class)
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun HomeScreen(
navigateToItemEntry: () -> Unit,
navigateToItemUpdate: (Int) -> Unit,
modifier: Modifier = Modifier,
viewModel: HomeViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val homeUiState by viewModel.homeUiState.collectAsState()
Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
InventoryTopAppBar(
title = stringResource(HomeDestination.titleRes),
canNavigateBack = false,
scrollBehavior = scrollBehavior
)
},
floatingActionButton = {
FloatingActionButton(
onClick = navigateToItemEntry,
shape = MaterialTheme.shapes.medium,
modifier = Modifier.padding(dimensionResource(id = R.dimen.padding_large))
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = stringResource(R.string.item_entry_title)
)
}
},
) { innerPadding ->
HomeBody(
itemList = homeUiState.itemList,
onItemClick = navigateToItemUpdate,
modifier = modifier.padding(innerPadding).fillMaxSize()
)
}
}
@Composable
private fun HomeBody(
itemList: List<Item>, onItemClick: (Int) -> Unit, modifier: Modifier = Modifier
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
) {
if (itemList.isEmpty()) {
Text(
text = stringResource(R.string.no_item_description),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleLarge
)
} else {
InventoryList(
itemList = itemList,
onItemClick = { onItemClick(it.id) },
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.padding_small))
)
}
}
}
@Composable
private fun InventoryList(
itemList: List<Item>, onItemClick: (Item) -> Unit, modifier: Modifier = Modifier
) {
LazyColumn(modifier = modifier) {
items(items = itemList, key = { it.id }) { item ->
InventoryItem(item = item,
modifier = Modifier
.padding(dimensionResource(id = R.dimen.padding_small))
.clickable { onItemClick(item) })
}
}
}
@Composable
private fun InventoryItem(
item: Item, modifier: Modifier = Modifier
) {
Card(
modifier = modifier,
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(dimensionResource(id = R.dimen.padding_large)),
verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.padding_small))
) {
Row(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = item.name,
style = MaterialTheme.typography.titleLarge,
)
Spacer(Modifier.weight(1f))
Text(
text = item.formatedPrice(),
style = MaterialTheme.typography.titleMedium
)
}
Text(
text = stringResource(R.string.in_stock, item.quantity),
style = MaterialTheme.typography.titleMedium
)
}
}
}
@Preview(showBackground = true)
@Composable
fun HomeBodyPreview() {
InventoryTheme {
HomeBody(listOf(
Item(1, "Game", 100.0, 20), Item(2, "Pen", 200.0, 30), Item(3, "TV", 300.0, 50)
), onItemClick = {})
}
}
@Preview(showBackground = true)
@Composable
fun HomeBodyEmptyListPreview() {
InventoryTheme {
HomeBody(listOf(), onItemClick = {})
}
}
@Preview(showBackground = true)
@Composable
fun InventoryItemPreview() {
InventoryTheme {
InventoryItem(
Item(1, "Game", 100.0, 20),
)
}
}
15. 显示商品信息ui/navigation/InventoryNavGraph.kt
package com.example.inventory.ui.navigation
@Composable
fun InventoryNavHost(
navController: NavHostController,
modifier: Modifier = Modifier,
) {
NavHost(
navController = navController,
startDestination = HomeDestination.route,
modifier = modifier
) {
composable(route = HomeDestination.route) {
HomeScreen(
navigateToItemEntry = { navController.navigate(ItemEntryDestination.route) },
navigateToItemUpdate = {
navController.navigate("ItemDetailsDestination.route/{it}")
}
)
}
composable(route = ItemEntryDestination.route) {
ItemEntryScreen(
navigateBack = { navController.popBackStack() },
onNavigateUp = { navController.navigateUp() }
)
}
composable(
route = ItemDetailsDestination.routeWithArgs,
arguments = listOf(navArgument(ItemDetailsDestination.itemIdArg) {
type = NavType.IntType
})
) {
ItemDetailsScreen(
navigateToEditItem = { navController.navigate("ItemEditDestination.route/it") },
navigateBack = { navController.navigateUp() }
)
}
composable(
route = ItemEditDestination.routeWithArgs,
arguments = listOf(navArgument(ItemEditDestination.itemIdArg) {
type = NavType.IntType
})
) {
ItemEditScreen(
navigateBack = { navController.popBackStack() },
onNavigateUp = { navController.navigateUp() }
)
}
}
}
二、 实验结果(含程序运行截图)
(1)没有商品的Inventory界面
(2)所有文本字段中的文本均有效(非空),则isEntryValid值为true,系统才会启用Save按钮
(3)运行应用,输入并保存数据,应用将返回商品目录界面,此操作会保存数据,显示在数据库中的数据(系统会显示商品目录列表)
(4)当点击 Inventory界面上的任何列表元素时,系统会显示Item Details界面,会显示从商品目录数据库中检索到的实体详情:
(5)在Inventory界面上,点击列表元素,当Item Details 界面显示时,点按Sell,您会注意到数量值减少了1;运行应用,请注意,当库存数量为零时,应用会停用 Sell 按钮
(6)运行应用,在 Inventory 界面上选择一个列表元素,在 Item Details 界面中,点按 Delete ,点按提醒对话框中的 Yes,应用会 返回到 Inventory 界面,确认已删除的实体不再位于应用数 据库中
(7)运行应用后,转到 Item Details 界面,然后点击浮动按钮,可以看到界面标题现在为 EditItem,不过,所有文本字段均为空,接下来,我们将使用实体详情填充 Edit Item 界面 中的文本字段
三、 出现问题及解决方法
问题1: 数据模型与数据库表映射问题,在定义数据模型(Entity)时,需要确保每个字段的注解正确无误,并且与数据库表的列相对应。有时候,由于字段的注解不正确或遗漏,导致编译时或运行时出现错误。
解决方案:仔细检查每个字段的注解,确保它们与数据库表的列相对应。使用Room提供的注解来指定主键、外键、索引等。
问题2:DAO方法的编写,在定义DAO接口时,需要编写用于增删改查的方法。
问题3:编译错误,在编译时,遇到与Room相关的错误,如无法识别注解、实体类定义错误等。
解决方法:确保你的项目已经添加了Room的依赖,并且同步了Gradle配置;检查实体类的注解是否正确,如@Entity、@PrimaryKey等;如果使用了Kotlin,确保Kotlin的kapt插件已经启用,并且正确配置了注解处理器。
问题4:运行时异常,在运行时,应用崩溃并抛出与Room相关的异常。
四、 实验心得
通过这次实验,我深刻体会到了Room数据库框架的便利性和强大功能,同时也对数据库操作有了更深入的理解。
首先,Room数据库的引入极大地简化了Android应用中的数据库操作。相比传统的数据库操作方式,Room提供了更简洁、更易于理解的API,使得我们可以更加专注于业务逻辑的实现,而不是陷入繁琐的数据库操作中。通过定义Entity、Dao和Database等组件,可以轻松地创建和操作数据库,实现数据的增删改查功能。
其次,在实验过程中,我逐渐掌握了Room数据库的基本用法和最佳实践。例如,通过使用=ViewModel,我能够实时观察数据库数据的变化,并在UI层进行相应的更新。这种响应式编程的方式使得应用更加灵活和高效。此外,我还学会了如何优化数据库操作,如使用事务批量处理数据、避免在UI线程中执行耗时操作等。
在Inventory应用中使用Room实现库存商品信息的列表、入库、修改、删除、显示详情功能的具体做法:
1. 定义实体类(Entity),首先需要定义一个表示库存商品信息的实体类。这个类将映射到数据库中的一张表。
2. 创建数据访问对象(DAO),它将包含与数据库交互的方法。这些方法将使用Room提供的注解来映射到SQL查询。
3. 创建数据库类(Database)创建一个继承自RoomDatabase的抽象类,在这个类中声明一个或多个抽象方法,每个方法都返回一个DAO接口类型的实例。
4. 注解和配置:使用@Database注解来指定数据库的名称、版本以及包含的实体类。
5. 构建数据库:在应用程序的适当位置(通常是Application类或初始化部分),使用Room.databaseBuilder()来构建并获取RoomDatabase的实例。
6. 初始化Room数据库实例
7. 为了更好地管理数据流动和逻辑分离,创建ViewModel和Repository类。ViewModel负责在UI和数据之间传递数据,而Repository负责封装数据访问逻辑
8. 在Activity或Fragment中初始化ViewModel,并使用它来执行数据库操作,通过ViewModelProviders完成
9. 在Activity或Fragment中,设置UI监听器来处理用户交互。当用户执行添加、修改、删除或查看详情操作时,调用ViewModel中相应的方法来更新数据库。
10. 在Activity中设置UI组件以展示库存商品信息并响应用户交互;将ViewModel与UI组件绑定,设置数据适配器,并配置点击事件监听器。
11. 在ViewModel中处理异步操作的结果,并将这些结果传递给UI