diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e8915595..79efc7f6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -19,6 +19,8 @@ plugins { id("org.jetbrains.kotlin.android") id("com.google.devtools.ksp") version "2.1.0-1.0.29" id("org.jetbrains.kotlin.plugin.compose") + //Room + } android { @@ -79,4 +81,9 @@ dependencies { // Testing androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") androidTestImplementation("androidx.test.ext:junit:1.2.1") + + //Room + implementation("androidx.room:room-runtime:2.8.3") + ksp("androidx.room:room-compiler:${rootProject.extra["room_version"]}") +// implementation("android.room:room-ktx:${rootProject.extra["room_version"]}") } diff --git a/app/src/main/java/com/example/inventory/InventoryApplication.kt b/app/src/main/java/com/example/inventory/InventoryApplication.kt index c0d8c1d5..14b6bd7a 100644 --- a/app/src/main/java/com/example/inventory/InventoryApplication.kt +++ b/app/src/main/java/com/example/inventory/InventoryApplication.kt @@ -29,6 +29,6 @@ class InventoryApplication : Application() { override fun onCreate() { super.onCreate() - container = AppDataContainer(this) + container = AppDataContainer(context = this) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/inventory/data/AppContainer.kt b/app/src/main/java/com/example/inventory/data/AppContainer.kt index 0d4414c8..e73c3769 100644 --- a/app/src/main/java/com/example/inventory/data/AppContainer.kt +++ b/app/src/main/java/com/example/inventory/data/AppContainer.kt @@ -33,6 +33,6 @@ class AppDataContainer(private val context: Context) : AppContainer { * Implementation for [ItemsRepository] */ override val itemsRepository: ItemsRepository by lazy { - OfflineItemsRepository() + OfflineItemsRepository(InventoryDatabase.getDatabase(context).itemDao()) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/inventory/data/InventoryDatabase.kt b/app/src/main/java/com/example/inventory/data/InventoryDatabase.kt new file mode 100644 index 00000000..416684f8 --- /dev/null +++ b/app/src/main/java/com/example/inventory/data/InventoryDatabase.kt @@ -0,0 +1,28 @@ +package com.example.inventory.data + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +@Database(entities = [Item::class], version = 1, exportSchema = false) +abstract class InventoryDatabase: RoomDatabase() { + abstract fun itemDao(): ItemDao + + companion object { + @Volatile + private var Instance: InventoryDatabase? = null + fun getDatabase(context: Context): InventoryDatabase { + return Instance ?: synchronized(this) { + Room.databaseBuilder( + context = context, + InventoryDatabase::class.java, + "item_database" + ) + .fallbackToDestructiveMigration(false) + .build() + .also { Instance = it } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/inventory/data/Item.kt b/app/src/main/java/com/example/inventory/data/Item.kt index eeef6bf5..6150e17e 100644 --- a/app/src/main/java/com/example/inventory/data/Item.kt +++ b/app/src/main/java/com/example/inventory/data/Item.kt @@ -16,11 +16,16 @@ package com.example.inventory.data +import androidx.room.Entity +import androidx.room.PrimaryKey + /** * Entity data class represents a single row in the database. */ -class Item( +@Entity(tableName = "items") +data class Item( + @PrimaryKey(autoGenerate = true) val id: Int = 0, val name: String, val price: Double, diff --git a/app/src/main/java/com/example/inventory/data/ItemDao.kt b/app/src/main/java/com/example/inventory/data/ItemDao.kt new file mode 100644 index 00000000..a8cc9196 --- /dev/null +++ b/app/src/main/java/com/example/inventory/data/ItemDao.kt @@ -0,0 +1,27 @@ +package com.example.inventory.data + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import kotlinx.coroutines.flow.Flow + +@Dao +interface ItemDao { + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(item: Item) + + @Update + suspend fun update(item: Item) + + @Delete + suspend fun delete(item: Item) + + @Query("SELECT * FROM items WHERE id = :id") + fun getItem(id: Int): Flow + + @Query("SELECT * FROM items ORDER BY name ASC") + fun getAllItems(): Flow> +} \ No newline at end of file diff --git a/app/src/main/java/com/example/inventory/data/ItemsRepository.kt b/app/src/main/java/com/example/inventory/data/ItemsRepository.kt index b956ea6d..74b26d79 100644 --- a/app/src/main/java/com/example/inventory/data/ItemsRepository.kt +++ b/app/src/main/java/com/example/inventory/data/ItemsRepository.kt @@ -16,7 +16,34 @@ package com.example.inventory.data +import kotlinx.coroutines.flow.Flow + /** * Repository that provides insert, update, delete, and retrieve of [Item] from a given data source. */ -interface ItemsRepository +interface ItemsRepository { + /** + * Retrieve all the items from the given data source + */ + fun getAllItemsStream(): Flow> + + /** + * Retrieve an item from the given data source that matches the given [id] + */ + fun getItemStream(id: Int): Flow + + /** + * Insert [item] in the data source + */ + suspend fun insertItem(item: Item) + + /** + * Delete [item] from the data source + */ + suspend fun deleteItem(item: Item) + + /** + * Update [item] in the data source + */ + suspend fun updateItem(item: Item) +} diff --git a/app/src/main/java/com/example/inventory/data/OfflineItemsRepository.kt b/app/src/main/java/com/example/inventory/data/OfflineItemsRepository.kt index d621a8f4..b0d06b0e 100644 --- a/app/src/main/java/com/example/inventory/data/OfflineItemsRepository.kt +++ b/app/src/main/java/com/example/inventory/data/OfflineItemsRepository.kt @@ -16,4 +16,17 @@ package com.example.inventory.data -class OfflineItemsRepository : ItemsRepository +import kotlinx.coroutines.flow.Flow + +class OfflineItemsRepository(private val itemDao: ItemDao) : ItemsRepository { + override fun getAllItemsStream(): Flow> = itemDao.getAllItems() + + override fun getItemStream(id: Int): Flow = itemDao.getItem(id) + + override suspend fun insertItem(item: Item) = itemDao.insert(item = item) + + override suspend fun deleteItem(item: Item) = itemDao.delete(item = item) + + override suspend fun updateItem(item: Item) = itemDao.update(item = item) +} + diff --git a/app/src/main/java/com/example/inventory/ui/AppViewModelProvider.kt b/app/src/main/java/com/example/inventory/ui/AppViewModelProvider.kt index 8aa8b481..8d3305a5 100644 --- a/app/src/main/java/com/example/inventory/ui/AppViewModelProvider.kt +++ b/app/src/main/java/com/example/inventory/ui/AppViewModelProvider.kt @@ -41,7 +41,7 @@ object AppViewModelProvider { } // Initializer for ItemEntryViewModel initializer { - ItemEntryViewModel() + ItemEntryViewModel(inventoryApplication().container.itemsRepository) } // Initializer for ItemDetailsViewModel diff --git a/app/src/main/java/com/example/inventory/ui/home/HomeScreen.kt b/app/src/main/java/com/example/inventory/ui/home/HomeScreen.kt index f3cda809..be471795 100644 --- a/app/src/main/java/com/example/inventory/ui/home/HomeScreen.kt +++ b/app/src/main/java/com/example/inventory/ui/home/HomeScreen.kt @@ -146,7 +146,8 @@ private fun InventoryList( InventoryItem(item = item, modifier = Modifier .padding(dimensionResource(id = R.dimen.padding_small)) - .clickable { onItemClick(item) }) + .clickable { onItemClick(item) } + ) } } } diff --git a/app/src/main/java/com/example/inventory/ui/item/ItemEntryScreen.kt b/app/src/main/java/com/example/inventory/ui/item/ItemEntryScreen.kt index 565f3e7c..c5912516 100644 --- a/app/src/main/java/com/example/inventory/ui/item/ItemEntryScreen.kt +++ b/app/src/main/java/com/example/inventory/ui/item/ItemEntryScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.dimensionResource @@ -45,6 +46,7 @@ import com.example.inventory.R import com.example.inventory.ui.AppViewModelProvider import com.example.inventory.ui.navigation.NavigationDestination import com.example.inventory.ui.theme.InventoryTheme +import kotlinx.coroutines.launch import java.util.Currency import java.util.Locale @@ -59,21 +61,27 @@ fun ItemEntryScreen( navigateBack: () -> Unit, onNavigateUp: () -> Unit, canNavigateBack: Boolean = true, - viewModel: ItemEntryViewModel = viewModel(factory = AppViewModelProvider.Factory) + viewModel: ItemEntryViewModel = viewModel(factory = AppViewModelProvider.Factory), ) { + val coroutineScope = rememberCoroutineScope() Scaffold( topBar = { InventoryTopAppBar( title = stringResource(ItemEntryDestination.titleRes), canNavigateBack = canNavigateBack, - navigateUp = onNavigateUp + navigateUp = onNavigateUp, ) } ) { innerPadding -> ItemEntryBody( itemUiState = viewModel.itemUiState, onItemValueChange = viewModel::updateUiState, - onSaveClick = { }, + onSaveClick = { + coroutineScope.launch { + viewModel.saveItem() + navigateBack() + } + }, modifier = Modifier .padding( start = innerPadding.calculateStartPadding(LocalLayoutDirection.current), @@ -81,7 +89,7 @@ fun ItemEntryScreen( top = innerPadding.calculateTopPadding() ) .verticalScroll(rememberScrollState()) - .fillMaxWidth() + .fillMaxWidth(), ) } } @@ -91,11 +99,11 @@ fun ItemEntryBody( itemUiState: ItemUiState, onItemValueChange: (ItemDetails) -> Unit, onSaveClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Column( verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.padding_large)), - modifier = modifier.padding(dimensionResource(id = R.dimen.padding_medium)) + modifier = modifier.padding(dimensionResource(id = R.dimen.padding_medium)), ) { ItemInputForm( itemDetails = itemUiState.itemDetails, @@ -175,7 +183,7 @@ fun ItemInputForm( } } -@Preview(showBackground = true) +@Preview(showBackground = true) // Preview @Composable private fun ItemEntryScreenPreview() { InventoryTheme { diff --git a/app/src/main/java/com/example/inventory/ui/item/ItemEntryViewModel.kt b/app/src/main/java/com/example/inventory/ui/item/ItemEntryViewModel.kt index 22915048..3795836e 100644 --- a/app/src/main/java/com/example/inventory/ui/item/ItemEntryViewModel.kt +++ b/app/src/main/java/com/example/inventory/ui/item/ItemEntryViewModel.kt @@ -21,12 +21,13 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import com.example.inventory.data.Item +import com.example.inventory.data.ItemsRepository import java.text.NumberFormat /** * ViewModel to validate and insert items in the Room database. */ -class ItemEntryViewModel : ViewModel() { +class ItemEntryViewModel(private val itemsRepository: ItemsRepository) : ViewModel() { /** * Holds current item ui state @@ -48,6 +49,12 @@ class ItemEntryViewModel : ViewModel() { name.isNotBlank() && price.isNotBlank() && quantity.isNotBlank() } } + + suspend fun saveItem() { + if (validateInput()) { + itemsRepository.insertItem(itemUiState.itemDetails.toItem()) + } + } } /** @@ -97,4 +104,4 @@ fun Item.toItemDetails(): ItemDetails = ItemDetails( name = name, price = price.toString(), quantity = quantity.toString() -) +) \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index f005d7e9..840acbc7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,14 +20,13 @@ buildscript { set("room_version", "2.6.0") } } - plugins { - id("com.android.application") version "8.8.0" apply false - id("com.android.library") version "8.8.0" apply false + id("com.android.application") version "8.13.1" apply false + id("com.android.library") version "8.13.1" apply false id("org.jetbrains.kotlin.android") version "2.1.0" apply false id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" apply false } tasks.register("clean", Delete::class) { - delete(rootProject.buildDir) + delete(rootProject.layout.buildDirectory) } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cea7a793..37f853b1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME