使用Room在Inventory应用中实现数据的持久化

一、实验名称

 使用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

  • 50
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值