diff --git a/README.md b/README.md index fbaa35b6..e27ae0e2 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,83 @@ -Inventory app +TESTMESSAGE + +To Do List app ================================== -Solution code for Android Basics with Compose. -Introduction + +問題一 ------------ -This app is an Inventory tracking app. Demos how to add, update, sell, and delete items from the local database. -This app demonstrated the use of Android Jetpack component [Room](https://developer.android.com/training/data-storage/room) database. -The app also leverages [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel), -[Flow](https://developer.android.com/kotlin/flow), -and [Navigation](https://developer.android.com/topic/libraries/architecture/navigation/). +*clone下來的存貨APP無法開啟* -Pre-requisites --------------- +## 解決方法 + +更改虛擬機的版本以及系統 + + + +## 問題二 + +*變數 函式名 檔案名在重構的過程中有許多問題 或是在修改後不能使用* + +## 解決方法 + +用Git進行版本控制 在每個重大改變前先commit + + + +## 問題三 + +*修改 Enity後程式運行會崩潰* + +## 解決方法 + +查看log cat 找到崩潰原因 + +```kotlin +android.database.sqlite.SQLiteException: […] +no such column: priority (code 1 SQLITE_ERROR): + , while compiling: SELECT * FROM tasks ORDER BY name ASC +``` -You need to know: -- How to create and use composables. -- How to navigate between composables, and pass data between them. -- How to use architecture components including ViewModel, Flow, StateFlow and StateUi. -- How to use coroutines for long-running tasks. -- SQLite database and the SQLite query language +詢問ChatGPT後是Room的語法問題 +```kotlin +@Database(entities = [Task::class], version = 2, exportSchema = false) -Getting Started ---------------- +//需要將version改成2 +abstract class AppDatabase : RoomDatabase() { + // … + companion object { + fun create(context: Context): AppDatabase = + Room.databaseBuilder(context, AppDatabase::class.java, "app.db") + .fallbackToDestructiveMigration() // 直接砍掉舊 DB,重建新結構 + .build() + } +} +``` -1. Download and run the app. +## 問題四 + +*無法使用swipe元件* + +## 解決方法 + +```kotlin + implementation("androidx.compose.material:material:1.8.1") +``` + +修改gradle檔 導入相依元件 + + + + + + + + + + + + +-------------- diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b9008c76..3b122b29 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -75,7 +75,9 @@ dependencies { implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7") implementation("androidx.navigation:navigation-compose:2.8.4") - + implementation(platform("androidx.compose:compose-bom:2025.02.00")) + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.material:material:1.8.1") //Room implementation("androidx.room:room-runtime:${rootProject.extra["room_version"]}") implementation("androidx.core:core-ktx:1.15.0") diff --git a/app/src/androidTest/java/com/example/inventory/ItemDaoTest.kt b/app/src/androidTest/java/com/example/inventory/TaskDaoTest.kt similarity index 70% rename from app/src/androidTest/java/com/example/inventory/ItemDaoTest.kt rename to app/src/androidTest/java/com/example/inventory/TaskDaoTest.kt index da73558e..15330f4b 100644 --- a/app/src/androidTest/java/com/example/inventory/ItemDaoTest.kt +++ b/app/src/androidTest/java/com/example/inventory/TaskDaoTest.kt @@ -21,8 +21,8 @@ import androidx.room.Room import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.example.inventory.data.InventoryDatabase -import com.example.inventory.data.Item -import com.example.inventory.data.ItemDao +import com.example.inventory.data.Task +import com.example.inventory.data.TaskDao import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.junit.After @@ -34,12 +34,12 @@ import org.junit.runner.RunWith import java.io.IOException @RunWith(AndroidJUnit4::class) -class ItemDaoTest { +class TaskDaoTest { - private lateinit var itemDao: ItemDao + private lateinit var taskDao: TaskDao private lateinit var inventoryDatabase: InventoryDatabase - private val item1 = Item(1, "Apples", 10.0, 20) - private val item2 = Item(2, "Bananas", 15.0, 97) + private val task1 = Task(1, "Apples", 10.0, 20) + private val task2 = Task(2, "Bananas", 15.0, 97) @Before fun createDb() { @@ -50,7 +50,7 @@ class ItemDaoTest { // Allowing main thread queries, just for testing. .allowMainThreadQueries() .build() - itemDao = inventoryDatabase.itemDao() + taskDao = inventoryDatabase.taskDao() } @After @@ -63,17 +63,17 @@ class ItemDaoTest { @Throws(Exception::class) fun daoInsert_insertsItemIntoDB() = runBlocking { addOneItemToDb() - val allItems = itemDao.getAllItems().first() - assertEquals(allItems[0], item1) + val allItems = taskDao.getAllItems().first() + assertEquals(allItems[0], task1) } @Test @Throws(Exception::class) fun daoGetAllItems_returnsAllItemsFromDB() = runBlocking { addTwoItemsToDb() - val allItems = itemDao.getAllItems().first() - assertEquals(allItems[0], item1) - assertEquals(allItems[1], item2) + val allItems = taskDao.getAllItems().first() + assertEquals(allItems[0], task1) + assertEquals(allItems[1], task2) } @@ -81,17 +81,17 @@ class ItemDaoTest { @Throws(Exception::class) fun daoGetItem_returnsItemFromDB() = runBlocking { addOneItemToDb() - val item = itemDao.getItem(1) - assertEquals(item.first(), item1) + val item = taskDao.getItem(1) + assertEquals(item.first(), task1) } @Test @Throws(Exception::class) fun daoDeleteItems_deletesAllItemsFromDB() = runBlocking { addTwoItemsToDb() - itemDao.delete(item1) - itemDao.delete(item2) - val allItems = itemDao.getAllItems().first() + taskDao.delete(task1) + taskDao.delete(task2) + val allItems = taskDao.getAllItems().first() assertTrue(allItems.isEmpty()) } @@ -99,20 +99,20 @@ class ItemDaoTest { @Throws(Exception::class) fun daoUpdateItems_updatesItemsInDB() = runBlocking { addTwoItemsToDb() - itemDao.update(Item(1, "Apples", 15.0, 25)) - itemDao.update(Item(2, "Bananas", 5.0, 50)) + taskDao.update(Task(1, "Apples", 15.0, 25)) + taskDao.update(Task(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)) + val allItems = taskDao.getAllItems().first() + assertEquals(allItems[0], Task(1, "Apples", 15.0, 25)) + assertEquals(allItems[1], Task(2, "Bananas", 5.0, 50)) } private suspend fun addOneItemToDb() { - itemDao.insert(item1) + taskDao.insert(task1) } private suspend fun addTwoItemsToDb() { - itemDao.insert(item1) - itemDao.insert(item2) + taskDao.insert(task1) + taskDao.insert(task2) } } 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 fc352312..08a924ef 100644 --- a/app/src/main/java/com/example/inventory/data/AppContainer.kt +++ b/app/src/main/java/com/example/inventory/data/AppContainer.kt @@ -22,17 +22,17 @@ import android.content.Context * App container for Dependency injection. */ interface AppContainer { - val itemsRepository: ItemsRepository + val tasksRepository: TasksRepository } /** - * [AppContainer] implementation that provides instance of [OfflineItemsRepository] + * [AppContainer] implementation that provides instance of [OfflineTasksRepository] */ class AppDataContainer(private val context: Context) : AppContainer { /** - * Implementation for [ItemsRepository] + * Implementation for [TasksRepository] */ - override val itemsRepository: ItemsRepository by lazy { - OfflineItemsRepository(InventoryDatabase.getDatabase(context).itemDao()) + override val tasksRepository: TasksRepository by lazy { + OfflineTasksRepository(InventoryDatabase.getDatabase(context).taskDao()) } } diff --git a/app/src/main/java/com/example/inventory/data/InventoryDatabase.kt b/app/src/main/java/com/example/inventory/data/InventoryDatabase.kt index d9977d9c..b8d294d0 100644 --- a/app/src/main/java/com/example/inventory/data/InventoryDatabase.kt +++ b/app/src/main/java/com/example/inventory/data/InventoryDatabase.kt @@ -24,10 +24,10 @@ import androidx.room.RoomDatabase /** * Database class with a singleton Instance object. */ -@Database(entities = [Item::class], version = 1, exportSchema = false) +@Database(entities = [Task::class], version = 2, exportSchema = false) abstract class InventoryDatabase : RoomDatabase() { - abstract fun itemDao(): ItemDao + abstract fun taskDao(): TaskDao companion object { @Volatile diff --git a/app/src/main/java/com/example/inventory/data/OfflineItemsRepository.kt b/app/src/main/java/com/example/inventory/data/OfflineTasksRepository.kt similarity index 63% rename from app/src/main/java/com/example/inventory/data/OfflineItemsRepository.kt rename to app/src/main/java/com/example/inventory/data/OfflineTasksRepository.kt index ed4c03b7..ac76fdf1 100644 --- a/app/src/main/java/com/example/inventory/data/OfflineItemsRepository.kt +++ b/app/src/main/java/com/example/inventory/data/OfflineTasksRepository.kt @@ -18,14 +18,14 @@ package com.example.inventory.data import kotlinx.coroutines.flow.Flow -class OfflineItemsRepository(private val itemDao: ItemDao) : ItemsRepository { - override fun getAllItemsStream(): Flow> = itemDao.getAllItems() +class OfflineTasksRepository(private val taskDao: TaskDao) : TasksRepository { + override fun getAllTasksStream(): Flow> = taskDao.getAllItems() - override fun getItemStream(id: Int): Flow = itemDao.getItem(id) + override fun getTaskStream(id: Int): Flow = taskDao.getItem(id) - override suspend fun insertItem(item: Item) = itemDao.insert(item) + override suspend fun insertTask(task: Task) = taskDao.insert(task) - override suspend fun deleteItem(item: Item) = itemDao.delete(item) + override suspend fun deleteTask(task: Task) = taskDao.delete(task) - override suspend fun updateItem(item: Item) = itemDao.update(item) + override suspend fun updateTask(task: Task) = taskDao.update(task) } diff --git a/app/src/main/java/com/example/inventory/data/Item.kt b/app/src/main/java/com/example/inventory/data/Task.kt similarity index 90% rename from app/src/main/java/com/example/inventory/data/Item.kt rename to app/src/main/java/com/example/inventory/data/Task.kt index edad7cae..cafb9ac4 100644 --- a/app/src/main/java/com/example/inventory/data/Item.kt +++ b/app/src/main/java/com/example/inventory/data/Task.kt @@ -22,11 +22,10 @@ import androidx.room.PrimaryKey /** * Entity data class represents a single row in the database. */ -@Entity(tableName = "items") -data class Item( +@Entity(tableName = "tasks") +data class Task( @PrimaryKey(autoGenerate = true) val id: Int = 0, val name: String, - val price: Double, - val quantity: Int + val priority: String ) diff --git a/app/src/main/java/com/example/inventory/data/ItemDao.kt b/app/src/main/java/com/example/inventory/data/TaskDao.kt similarity index 79% rename from app/src/main/java/com/example/inventory/data/ItemDao.kt rename to app/src/main/java/com/example/inventory/data/TaskDao.kt index 22b14c72..e058f569 100644 --- a/app/src/main/java/com/example/inventory/data/ItemDao.kt +++ b/app/src/main/java/com/example/inventory/data/TaskDao.kt @@ -28,22 +28,22 @@ import kotlinx.coroutines.flow.Flow * Database access object to access the Inventory database */ @Dao -interface ItemDao { +interface TaskDao { - @Query("SELECT * from items ORDER BY name ASC") - fun getAllItems(): Flow> + @Query("SELECT * from tasks ORDER BY name ASC") + fun getAllItems(): Flow> - @Query("SELECT * from items WHERE id = :id") - fun getItem(id: Int): Flow + @Query("SELECT * from tasks WHERE id = :id") + fun getItem(id: Int): Flow // Specify the conflict strategy as IGNORE, when the user tries to add an // existing Item into the database Room ignores the conflict. @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insert(item: Item) + suspend fun insert(task: Task) @Update - suspend fun update(item: Item) + suspend fun update(task: Task) @Delete - suspend fun delete(item: Item) + suspend fun delete(task: Task) } diff --git a/app/src/main/java/com/example/inventory/data/ItemsRepository.kt b/app/src/main/java/com/example/inventory/data/TasksRepository.kt similarity index 80% rename from app/src/main/java/com/example/inventory/data/ItemsRepository.kt rename to app/src/main/java/com/example/inventory/data/TasksRepository.kt index 57029541..873cee79 100644 --- a/app/src/main/java/com/example/inventory/data/ItemsRepository.kt +++ b/app/src/main/java/com/example/inventory/data/TasksRepository.kt @@ -19,31 +19,31 @@ 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. + * Repository that provides insert, update, delete, and retrieve of [Task] from a given data source. */ -interface ItemsRepository { +interface TasksRepository { /** * Retrieve all the items from the the given data source. */ - fun getAllItemsStream(): Flow> + fun getAllTasksStream(): Flow> /** * Retrieve an item from the given data source that matches with the [id]. */ - fun getItemStream(id: Int): Flow + fun getTaskStream(id: Int): Flow /** * Insert item in the data source */ - suspend fun insertItem(item: Item) + suspend fun insertTask(task: Task) /** * Delete item from the data source */ - suspend fun deleteItem(item: Item) + suspend fun deleteTask(task: Task) /** * Update item in the data source */ - suspend fun updateItem(item: Item) + suspend fun updateTask(task: Task) } 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 be50e541..bc1c8157 100644 --- a/app/src/main/java/com/example/inventory/ui/AppViewModelProvider.kt +++ b/app/src/main/java/com/example/inventory/ui/AppViewModelProvider.kt @@ -24,9 +24,9 @@ import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import com.example.inventory.InventoryApplication import com.example.inventory.ui.home.HomeViewModel -import com.example.inventory.ui.item.ItemDetailsViewModel -import com.example.inventory.ui.item.ItemEditViewModel -import com.example.inventory.ui.item.ItemEntryViewModel +import com.example.inventory.ui.task.TaskDetailsViewModel +import com.example.inventory.ui.task.TaskEditViewModel +import com.example.inventory.ui.task.ItemEntryViewModel /** * Provides Factory to create instance of ViewModel for the entire Inventory app @@ -35,27 +35,27 @@ object AppViewModelProvider { val Factory = viewModelFactory { // Initializer for ItemEditViewModel initializer { - ItemEditViewModel( + TaskEditViewModel( this.createSavedStateHandle(), - inventoryApplication().container.itemsRepository + inventoryApplication().container.tasksRepository ) } // Initializer for ItemEntryViewModel initializer { - ItemEntryViewModel(inventoryApplication().container.itemsRepository) + ItemEntryViewModel(inventoryApplication().container.tasksRepository) } // Initializer for ItemDetailsViewModel initializer { - ItemDetailsViewModel( + TaskDetailsViewModel( this.createSavedStateHandle(), - inventoryApplication().container.itemsRepository + inventoryApplication().container.tasksRepository ) } // Initializer for HomeViewModel initializer { - HomeViewModel(inventoryApplication().container.itemsRepository) + HomeViewModel(inventoryApplication().container.tasksRepository) } } } 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 910552ce..5128ed1e 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 @@ -1,52 +1,25 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.example.inventory.ui.home +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.calculateEndPadding -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material.DismissDirection +import androidx.compose.material.DismissState +import androidx.compose.material.DismissValue +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.SwipeToDismiss import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.rememberDismissState +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.dimensionResource @@ -57,20 +30,17 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.example.inventory.InventoryTopAppBar import com.example.inventory.R -import com.example.inventory.data.Item +import com.example.inventory.data.Task import com.example.inventory.ui.AppViewModelProvider -import com.example.inventory.ui.item.formatedPrice import com.example.inventory.ui.navigation.NavigationDestination import com.example.inventory.ui.theme.InventoryTheme +import kotlinx.coroutines.launch object HomeDestination : NavigationDestination { override val route = "home" override val titleRes = R.string.app_name } -/** - * Entry route for Home screen - */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeScreen( @@ -82,6 +52,9 @@ fun HomeScreen( val homeUiState by viewModel.homeUiState.collectAsState() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + Scaffold( modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { @@ -91,6 +64,7 @@ fun HomeScreen( scrollBehavior = scrollBehavior ) }, + snackbarHost = { SnackbarHost(snackbarHostState) }, floatingActionButton = { FloatingActionButton( onClick = navigateToItemEntry, @@ -101,17 +75,23 @@ fun HomeScreen( .calculateEndPadding(LocalLayoutDirection.current) ) ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = stringResource(R.string.item_entry_title) - ) + Icon(Icons.Default.Add, contentDescription = stringResource(R.string.task_entry_title)) } }, ) { innerPadding -> HomeBody( - itemList = homeUiState.itemList, - onItemClick = navigateToItemUpdate, - modifier = modifier.fillMaxSize(), + taskList = homeUiState.taskList, + onTaskClick = navigateToItemUpdate, + onDelete = { task -> + viewModel.delete(task) + scope.launch { + val res = snackbarHostState.showSnackbar( + "已刪除 ${task.name}", actionLabel = "復原", duration = SnackbarDuration.Short + ) + if (res == SnackbarResult.ActionPerformed) viewModel.insert(task) + } + }, + modifier = Modifier.fillMaxSize(), contentPadding = innerPadding, ) } @@ -119,26 +99,25 @@ fun HomeScreen( @Composable private fun HomeBody( - itemList: List, - onItemClick: (Int) -> Unit, + taskList: List, + onTaskClick: (Int) -> Unit, + onDelete: (Task) -> Unit, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(0.dp), ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier, - ) { - if (itemList.isEmpty()) { + Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier) { + if (taskList.isEmpty()) { Text( - text = stringResource(R.string.no_item_description), + stringResource(R.string.no_task_description), textAlign = TextAlign.Center, style = MaterialTheme.typography.titleLarge, - modifier = Modifier.padding(contentPadding), + modifier = Modifier.padding(contentPadding) ) } else { InventoryList( - itemList = itemList, - onItemClick = { onItemClick(it.id) }, + taskList = taskList, + onItemClick = { onTaskClick(it.id) }, + onDelete = onDelete, contentPadding = contentPadding, modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.padding_small)) ) @@ -148,80 +127,102 @@ private fun HomeBody( @Composable private fun InventoryList( - itemList: List, - onItemClick: (Item) -> Unit, + taskList: List, + onItemClick: (Task) -> Unit, + onDelete: (Task) -> Unit, contentPadding: PaddingValues, modifier: Modifier = Modifier ) { - LazyColumn( - modifier = modifier, - contentPadding = contentPadding - ) { - items(items = itemList, key = { it.id }) { item -> - InventoryItem(item = item, - modifier = Modifier - .padding(dimensionResource(id = R.dimen.padding_small)) - .clickable { onItemClick(item) }) + LazyColumn(modifier = modifier, contentPadding = contentPadding) { + items(taskList, key = { it.id }) { task -> + DismissibleTask( + task = task, + onDelete = onDelete, + onClick = onItemClick, + modifier = Modifier.padding(dimensionResource(id = R.dimen.padding_small)) + ) } } } +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @Composable -private fun InventoryItem( - item: Item, modifier: Modifier = Modifier +fun DismissibleTask( + task: Task, + onDelete: (Task) -> Unit, + onClick: (Task) -> Unit, + 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 - ) + val dismissState = rememberDismissState(confirmStateChange = { newState: DismissValue -> + if (newState == DismissValue.DismissedToEnd || newState == DismissValue.DismissedToStart) { + onDelete(task) + true + } else false + }) + + SwipeToDismiss( + state = dismissState, + background = { SwipeBackground(dismissState) }, + dismissContent = { + InventoryItem(task, modifier.clickable { onClick(task) }) } - } + ) } -@Preview(showBackground = true) +@OptIn(ExperimentalMaterialApi::class) @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 = {}) +private fun SwipeBackground(state: DismissState) { + val bgColor by animateColorAsState( + if (state.targetValue == DismissValue.Default) MaterialTheme.colorScheme.surfaceVariant + else MaterialTheme.colorScheme.errorContainer + ) + val alignment = when (state.dismissDirection) { + DismissDirection.StartToEnd -> Alignment.CenterStart + DismissDirection.EndToStart -> Alignment.CenterEnd + else -> Alignment.Center + } + Box( + Modifier.fillMaxWidth().height(64.dp).background(bgColor).padding(horizontal = 20.dp), + contentAlignment = alignment + ) { + Icon(Icons.Default.Delete, contentDescription = null, + tint = MaterialTheme.colorScheme.onErrorContainer) } } -@Preview(showBackground = true) @Composable -fun HomeBodyEmptyListPreview() { - InventoryTheme { - HomeBody(listOf(), onItemClick = {}) +private fun InventoryItem(task: Task, modifier: Modifier = Modifier) { + val cardColor = when (task.priority.lowercase()) { + "high" -> MaterialTheme.colorScheme.errorContainer + "medium" -> Color(0xFFFFF9C4) + "low" -> Color(0xFFC8E6C9) + else -> MaterialTheme.colorScheme.surface + } + Card( + modifier, + elevation = CardDefaults.cardElevation(2.dp), + colors = CardDefaults.cardColors(containerColor = cardColor) + ) { + Column( + Modifier.padding(dimensionResource(id = R.dimen.padding_large)), + verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.padding_small)) + ) { + Row(Modifier.fillMaxWidth()) { + Text(task.name, style = MaterialTheme.typography.titleLarge) + Spacer(Modifier.weight(1f)) + Text(stringResource(R.string.in_stock, task.priority), style = MaterialTheme.typography.titleMedium) + } + } } } @Preview(showBackground = true) @Composable -fun InventoryItemPreview() { +fun PreviewHomeBody() { InventoryTheme { - InventoryItem( - Item(1, "Game", 100.0, 20), + HomeBody( + listOf(Task(1, "Task1", "High"), Task(2, "Task2", "Medium"), Task(3, "Task3", "Low")), + onTaskClick = {}, onDelete = {} ) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/inventory/ui/home/HomeViewModel.kt b/app/src/main/java/com/example/inventory/ui/home/HomeViewModel.kt index 3ec7087f..e68678dc 100644 --- a/app/src/main/java/com/example/inventory/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/example/inventory/ui/home/HomeViewModel.kt @@ -1,53 +1,22 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.example.inventory.ui.home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.inventory.data.Item -import com.example.inventory.data.ItemsRepository +import com.example.inventory.data.Task +import com.example.inventory.data.TasksRepository import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch -/** - * ViewModel to retrieve all items in the Room database. - */ -class HomeViewModel(itemsRepository: ItemsRepository) : ViewModel() { - - /** - * Holds home ui state. The list of items are retrieved from [ItemsRepository] and mapped to - * [HomeUiState] - */ - val homeUiState: StateFlow = - itemsRepository.getAllItemsStream().map { HomeUiState(it) } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS), - initialValue = HomeUiState() - ) +class HomeViewModel(private val repository: TasksRepository) : ViewModel() { + val homeUiState: StateFlow = repository.getAllTasksStream() + .map(::HomeUiState) + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), HomeUiState()) - companion object { - private const val TIMEOUT_MILLIS = 5_000L - } + fun delete(task: Task) = viewModelScope.launch { repository.deleteTask(task) } + fun insert(task: Task) = viewModelScope.launch { repository.insertTask(task) } } -/** - * Ui State for HomeScreen - */ -data class HomeUiState(val itemList: List = listOf()) +data class HomeUiState(val taskList: List = emptyList()) \ No newline at end of file diff --git a/app/src/main/java/com/example/inventory/ui/item/ItemDetailsScreen.kt b/app/src/main/java/com/example/inventory/ui/item/ItemDetailsScreen.kt deleted file mode 100644 index caab4181..00000000 --- a/app/src/main/java/com/example/inventory/ui/item/ItemDetailsScreen.kt +++ /dev/null @@ -1,270 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.inventory.ui.item - -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.calculateEndPadding -import androidx.compose.foundation.layout.calculateStartPadding -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Edit -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.res.dimensionResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.lifecycle.viewmodel.compose.viewModel -import com.example.inventory.InventoryTopAppBar -import com.example.inventory.R -import com.example.inventory.data.Item -import com.example.inventory.ui.AppViewModelProvider -import com.example.inventory.ui.navigation.NavigationDestination -import com.example.inventory.ui.theme.InventoryTheme -import kotlinx.coroutines.launch - -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( - end = WindowInsets.safeDrawing.asPaddingValues() - .calculateEndPadding(LocalLayoutDirection.current) - ) - ) { - Icon( - imageVector = Icons.Default.Edit, - contentDescription = stringResource(R.string.edit_item_title), - ) - } - }, - modifier = modifier, - ) { innerPadding -> - ItemDetailsBody( - itemDetailsUiState = uiState.value, - onSellItem = { viewModel.reduceQuantityByOne() }, - onDelete = { - // Note: If the user rotates the screen very fast, the operation may get cancelled - // and the item may not be deleted from the Database. This is because when config - // change occurs, the Activity will be recreated and the rememberCoroutineScope will - // be cancelled - since the scope is bound to composition. - coroutineScope.launch { - viewModel.deleteItem() - navigateBack() - } - }, - modifier = Modifier - .padding( - start = innerPadding.calculateStartPadding(LocalLayoutDirection.current), - top = innerPadding.calculateTopPadding(), - end = innerPadding.calculateEndPadding(LocalLayoutDirection.current), - ) - .verticalScroll(rememberScrollState()) - ) - } -} - -@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 = !itemDetailsUiState.outOfStock - ) { - 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(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(text = stringResource(R.string.no)) - } - }, - confirmButton = { - TextButton(onClick = onDeleteConfirm) { - Text(text = stringResource(R.string.yes)) - } - }) -} - -@Preview(showBackground = true) -@Composable -fun ItemDetailsScreenPreview() { - InventoryTheme { - ItemDetailsBody(ItemDetailsUiState( - outOfStock = true, itemDetails = ItemDetails(1, "Pen", "$100", "10") - ), onSellItem = {}, onDelete = {}) - } -} diff --git a/app/src/main/java/com/example/inventory/ui/item/ItemDetailsViewModel.kt b/app/src/main/java/com/example/inventory/ui/item/ItemDetailsViewModel.kt deleted file mode 100644 index c37b0f58..00000000 --- a/app/src/main/java/com/example/inventory/ui/item/ItemDetailsViewModel.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.inventory.ui.item - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.example.inventory.data.ItemsRepository -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 -import kotlinx.coroutines.launch - -/** - * ViewModel to retrieve, update and delete an item from the [ItemsRepository]'s data source. - */ -class ItemDetailsViewModel( - savedStateHandle: SavedStateHandle, - private val itemsRepository: ItemsRepository, -) : ViewModel() { - - private val itemId: Int = checkNotNull(savedStateHandle[ItemDetailsDestination.itemIdArg]) - - /** - * Holds the item details ui state. The data is retrieved from [ItemsRepository] and mapped to - * the UI state. - */ - val uiState: StateFlow = - itemsRepository.getItemStream(itemId) - .filterNotNull() - .map { - ItemDetailsUiState(outOfStock = it.quantity <= 0, itemDetails = it.toItemDetails()) - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS), - initialValue = ItemDetailsUiState() - ) - - /** - * Reduces the item quantity by one and update the [ItemsRepository]'s data source. - */ - fun reduceQuantityByOne() { - viewModelScope.launch { - val currentItem = uiState.value.itemDetails.toItem() - if (currentItem.quantity > 0) { - itemsRepository.updateItem(currentItem.copy(quantity = currentItem.quantity - 1)) - } - } - } - - /** - * Deletes the item from the [ItemsRepository]'s data source. - */ - suspend fun deleteItem() { - itemsRepository.deleteItem(uiState.value.itemDetails.toItem()) - } - - companion object { - private const val TIMEOUT_MILLIS = 5_000L - } -} - -/** - * UI state for ItemDetailsScreen - */ -data class ItemDetailsUiState( - val outOfStock: Boolean = true, - val itemDetails: ItemDetails = ItemDetails() -) diff --git a/app/src/main/java/com/example/inventory/ui/navigation/InventoryNavGraph.kt b/app/src/main/java/com/example/inventory/ui/navigation/InventoryNavGraph.kt index 1f56c32d..a499264d 100644 --- a/app/src/main/java/com/example/inventory/ui/navigation/InventoryNavGraph.kt +++ b/app/src/main/java/com/example/inventory/ui/navigation/InventoryNavGraph.kt @@ -25,12 +25,12 @@ import androidx.navigation.compose.composable import androidx.navigation.navArgument import com.example.inventory.ui.home.HomeDestination import com.example.inventory.ui.home.HomeScreen -import com.example.inventory.ui.item.ItemDetailsDestination -import com.example.inventory.ui.item.ItemDetailsScreen -import com.example.inventory.ui.item.ItemEditDestination -import com.example.inventory.ui.item.ItemEditScreen -import com.example.inventory.ui.item.ItemEntryDestination -import com.example.inventory.ui.item.ItemEntryScreen +import com.example.inventory.ui.task.TaskDetailsDestination +import com.example.inventory.ui.task.TaskDetailsScreen +import com.example.inventory.ui.task.ItemEditDestination +import com.example.inventory.ui.task.ItemEditScreen +import com.example.inventory.ui.task.ItemEntryDestination +import com.example.inventory.ui.task.ItemEntryScreen /** * Provides Navigation graph for the application. @@ -49,7 +49,7 @@ fun InventoryNavHost( HomeScreen( navigateToItemEntry = { navController.navigate(ItemEntryDestination.route) }, navigateToItemUpdate = { - navController.navigate("${ItemDetailsDestination.route}/${it}") + navController.navigate("${TaskDetailsDestination.route}/${it}") } ) } @@ -60,13 +60,13 @@ fun InventoryNavHost( ) } composable( - route = ItemDetailsDestination.routeWithArgs, - arguments = listOf(navArgument(ItemDetailsDestination.itemIdArg) { + route = TaskDetailsDestination.routeWithArgs, + arguments = listOf(navArgument(TaskDetailsDestination.taskIdArg) { type = NavType.IntType }) ) { - ItemDetailsScreen( - navigateToEditItem = { navController.navigate("${ItemEditDestination.route}/$it") }, + TaskDetailsScreen( + navigateToEditTask = { navController.navigate("${ItemEditDestination.route}/$it") }, navigateBack = { navController.navigateUp() } ) } diff --git a/app/src/main/java/com/example/inventory/ui/task/TaskDetailsScreen.kt b/app/src/main/java/com/example/inventory/ui/task/TaskDetailsScreen.kt new file mode 100644 index 00000000..db6faffc --- /dev/null +++ b/app/src/main/java/com/example/inventory/ui/task/TaskDetailsScreen.kt @@ -0,0 +1,320 @@ +// TaskDetailsScreen.kt +package com.example.inventory.ui.task + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.example.inventory.InventoryTopAppBar +import com.example.inventory.R +import com.example.inventory.data.Task +import com.example.inventory.ui.AppViewModelProvider +import com.example.inventory.ui.navigation.NavigationDestination +import com.example.inventory.ui.theme.InventoryTheme +import kotlinx.coroutines.launch + +object TaskDetailsDestination : NavigationDestination { + override val route = "task_details" + override val titleRes = R.string.task_detail_title + const val taskIdArg = "taskId" + val routeWithArgs = "$route/{$taskIdArg}" +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TaskDetailsScreen( + navigateToEditTask: (Int) -> Unit, + navigateBack: () -> Unit, + modifier: Modifier = Modifier, + viewModel: TaskDetailsViewModel = viewModel(factory = AppViewModelProvider.Factory) +) { + val uiState by viewModel.uiState.collectAsState() + val coroutineScope = rememberCoroutineScope() + + Scaffold( + topBar = { + InventoryTopAppBar( + title = stringResource(TaskDetailsDestination.titleRes), + canNavigateBack = true, + navigateUp = navigateBack + ) + }, + floatingActionButton = { + FloatingActionButton( + onClick = { navigateToEditTask(uiState.taskDetails.id) }, + shape = MaterialTheme.shapes.medium, + modifier = Modifier + .padding( + end = WindowInsets.safeDrawing.asPaddingValues() + .calculateEndPadding(LocalLayoutDirection.current) + ) + ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = stringResource(R.string.edit_task_title), + ) + } + }, + modifier = modifier, + ) { innerPadding -> + TaskDetailsBody( + taskDetailsUiState = uiState, + onDelete = { + coroutineScope.launch { + viewModel.deleteTask() + navigateBack() + } + }, + onConfirm = { newName, newPriority -> + viewModel.updateTask( + name = newName, + priority = newPriority + ) + }, + modifier = Modifier + .padding( + start = innerPadding.calculateStartPadding(LocalLayoutDirection.current), + top = innerPadding.calculateTopPadding(), + end = innerPadding.calculateEndPadding(LocalLayoutDirection.current), + ) + .verticalScroll(rememberScrollState()) + ) + } +} + +@Composable +private fun TaskDetailsBody( + taskDetailsUiState: TaskDetailsUiState, + onDelete: () -> Unit, + onConfirm: (String, String) -> 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) } + var editable by rememberSaveable { mutableStateOf(false) } + var inputText by rememberSaveable { mutableStateOf(taskDetailsUiState.taskDetails.name) } + var selectedPriority by rememberSaveable { mutableStateOf(taskDetailsUiState.taskDetails.priority) } + + // 原始卡片顯示 + TaskDetails( + task = taskDetailsUiState.taskDetails.toTask(), + modifier = Modifier.fillMaxWidth() + ) + + // 刪除按鈕 + OutlinedButton( + onClick = { deleteConfirmationRequired = true }, + shape = MaterialTheme.shapes.small, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.outlinedButtonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ) + ) { + Text(stringResource(R.string.delete)) + } + + // Edit / Cancel & Confirm + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + if (!editable) { + OutlinedButton(onClick = { editable = true }, modifier = Modifier.weight(1f)) { + Text(stringResource(R.string.edit)) + } + } else { + OutlinedButton(onClick = { + // rollback + editable = false + inputText = taskDetailsUiState.taskDetails.name + selectedPriority = taskDetailsUiState.taskDetails.priority + }, modifier = Modifier.weight(1f)) { + Text(stringResource(R.string.cancel)) + } + OutlinedButton(onClick = { + editable = false + onConfirm(inputText, selectedPriority) + }, modifier = Modifier.weight(1f)) { + Text(stringResource(R.string.confirm)) + } + } + } + + // 編輯輸入框 + OutlinedTextField( + value = inputText, + onValueChange = { inputText = it }, + label = { Text(stringResource(R.string.task)) }, + colors = OutlinedTextFieldDefaults.colors( + focusedContainerColor = MaterialTheme.colorScheme.secondaryContainer, + unfocusedContainerColor = MaterialTheme.colorScheme.secondaryContainer, + disabledContainerColor = MaterialTheme.colorScheme.secondaryContainer + ), + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + enabled = editable + ) + + // Priority RadioGroup + val priorities = listOf("High", "Medium", "Low") + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + priorities.forEach { level -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .weight(1f) + .selectable( + selected = (selectedPriority == level), + onClick = { if (editable) selectedPriority = level }, + role = Role.RadioButton + ) + ) { + RadioButton( + selected = (selectedPriority == level), + onClick = null, + enabled = editable + ) + Spacer(modifier = Modifier.width(dimensionResource(id = R.dimen.padding_small))) + Text( + level, + color = when (level.lowercase()) { + "high" -> MaterialTheme.colorScheme.error + "medium" -> Color(0xFFFBC02D) + else -> Color(0xFF388E3C) + }, + fontWeight = FontWeight.Bold + ) + } + } + } + + // 刪除確認對話框 + if (deleteConfirmationRequired) { + AlertDialog( + onDismissRequest = { /* no-op */ }, + title = { Text(stringResource(R.string.attention)) }, + text = { Text(stringResource(R.string.delete_question)) }, + confirmButton = { + TextButton(onClick = { + deleteConfirmationRequired = false + onDelete() + }) { + Text(stringResource(R.string.yes)) + } + }, + dismissButton = { + TextButton(onClick = { deleteConfirmationRequired = false }) { + Text(stringResource(R.string.no)) + } + } + ) + } + } +} + +@Composable +fun TaskDetails( + task: Task, + 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)) + ) { + TaskDetailsRow(R.string.task, task.name) + TaskDetailsRow(R.string.priority_level_show, task.priority) + } + } +} + +@Composable +private fun TaskDetailsRow( + @StringRes labelResID: Int, + taskDetail: String +) { + Row( + modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.padding_medium)) + ) { + Text(text = stringResource(labelResID)) + Spacer(modifier = Modifier.weight(1f)) + Text(text = taskDetail, fontWeight = FontWeight.Bold) + } +} + +@Preview(showBackground = true) +@Composable +fun TaskDetailsScreenPreview() { + InventoryTheme { + TaskDetailsBody( + taskDetailsUiState = TaskDetailsUiState( + taskDetails = TaskDetails(1, "示範任務", "Medium") + ), + onDelete = { /* no-op */ }, + onConfirm = { _, _ -> /* no-op */ } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/inventory/ui/task/TaskDetailsViewModel.kt b/app/src/main/java/com/example/inventory/ui/task/TaskDetailsViewModel.kt new file mode 100644 index 00000000..125c8025 --- /dev/null +++ b/app/src/main/java/com/example/inventory/ui/task/TaskDetailsViewModel.kt @@ -0,0 +1,73 @@ +package com.example.inventory.ui.task + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.inventory.data.TasksRepository +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 +import kotlinx.coroutines.launch + +/** + * ViewModel to retrieve, update and delete a task from the [TasksRepository]'s data source. + */ +class TaskDetailsViewModel( + savedStateHandle: SavedStateHandle, + private val tasksRepository: TasksRepository +) : ViewModel() { + + // 從 navigation arguments 拿到要顯示的 taskId + private val taskId: Int = checkNotNull(savedStateHandle[TaskDetailsDestination.taskIdArg]) + + /** + * uiState 會隨著資料庫中該筆 task 更新自動推送最新值 + */ + val uiState: StateFlow = + tasksRepository + .getTaskStream(taskId) // 回傳 Flow + .filterNotNull() // 避免 null + .map { entity -> + // Map Entity -> UI state + TaskDetailsUiState(taskDetails = entity.toItemDetails()) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS), + initialValue = TaskDetailsUiState() + ) + + /** + * 刪除該 task + */ + fun deleteTask() { + viewModelScope.launch { + tasksRepository.deleteTask(uiState.value.taskDetails.toTask()) + } + } + + /** + * 更新該 task 的 name 與 priority + */ + fun updateTask(name: String, priority: String) { + viewModelScope.launch { + // 構造一個新的 TaskData 並交給 repository 更新 + val updated = uiState.value.taskDetails.copy(name = name, priority = priority) + tasksRepository.updateTask(updated.toTask()) + // 不需要手動刷新,getTaskStream 會自動 emit 最新的資料 + } + } + + companion object { + private const val TIMEOUT_MILLIS = 5_000L + } +} + +/** + * UI state for TaskDetailsScreen + */ +data class TaskDetailsUiState( + val taskDetails: TaskDetails = TaskDetails() +) \ No newline at end of file diff --git a/app/src/main/java/com/example/inventory/ui/item/ItemEditScreen.kt b/app/src/main/java/com/example/inventory/ui/task/TaskEditScreen.kt similarity index 95% rename from app/src/main/java/com/example/inventory/ui/item/ItemEditScreen.kt rename to app/src/main/java/com/example/inventory/ui/task/TaskEditScreen.kt index e290a3c2..2da2d9a8 100644 --- a/app/src/main/java/com/example/inventory/ui/item/ItemEditScreen.kt +++ b/app/src/main/java/com/example/inventory/ui/task/TaskEditScreen.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.inventory.ui.item +package com.example.inventory.ui.task import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding @@ -39,7 +39,7 @@ import kotlinx.coroutines.launch object ItemEditDestination : NavigationDestination { override val route = "item_edit" - override val titleRes = R.string.edit_item_title + override val titleRes = R.string.edit_task_title const val itemIdArg = "itemId" val routeWithArgs = "$route/{$itemIdArg}" } @@ -50,7 +50,7 @@ fun ItemEditScreen( navigateBack: () -> Unit, onNavigateUp: () -> Unit, modifier: Modifier = Modifier, - viewModel: ItemEditViewModel = viewModel(factory = AppViewModelProvider.Factory) + viewModel: TaskEditViewModel = viewModel(factory = AppViewModelProvider.Factory) ) { val coroutineScope = rememberCoroutineScope() Scaffold( diff --git a/app/src/main/java/com/example/inventory/ui/item/ItemEditViewModel.kt b/app/src/main/java/com/example/inventory/ui/task/TaskEditViewModel.kt similarity index 68% rename from app/src/main/java/com/example/inventory/ui/item/ItemEditViewModel.kt rename to app/src/main/java/com/example/inventory/ui/task/TaskEditViewModel.kt index bdd64918..a302c987 100644 --- a/app/src/main/java/com/example/inventory/ui/item/ItemEditViewModel.kt +++ b/app/src/main/java/com/example/inventory/ui/task/TaskEditViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.inventory.ui.item +package com.example.inventory.ui.task import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -22,17 +22,17 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.inventory.data.ItemsRepository +import com.example.inventory.data.TasksRepository import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch /** - * ViewModel to retrieve and update an item from the [ItemsRepository]'s data source. + * ViewModel to retrieve and update an item from the [TasksRepository]'s data source. */ -class ItemEditViewModel( +class TaskEditViewModel( savedStateHandle: SavedStateHandle, - private val itemsRepository: ItemsRepository + private val tasksRepository: TasksRepository ) : ViewModel() { /** @@ -45,7 +45,7 @@ class ItemEditViewModel( init { viewModelScope.launch { - itemUiState = itemsRepository.getItemStream(itemId) + itemUiState = tasksRepository.getTaskStream(itemId) .filterNotNull() .first() .toItemUiState(true) @@ -53,11 +53,11 @@ class ItemEditViewModel( } /** - * Update the item in the [ItemsRepository]'s data source + * Update the item in the [TasksRepository]'s data source */ suspend fun updateItem() { - if (validateInput(itemUiState.itemDetails)) { - itemsRepository.updateItem(itemUiState.itemDetails.toItem()) + if (validateInput(itemUiState.taskDetails)) { + tasksRepository.updateTask(itemUiState.taskDetails.toTask()) } } @@ -65,14 +65,14 @@ class ItemEditViewModel( * Updates the [itemUiState] with the value provided in the argument. This method also triggers * a validation for input values. */ - fun updateUiState(itemDetails: ItemDetails) { + fun updateUiState(taskDetails: TaskDetails) { itemUiState = - ItemUiState(itemDetails = itemDetails, isEntryValid = validateInput(itemDetails)) + ItemUiState(taskDetails = taskDetails, isEntryValid = validateInput(taskDetails)) } - private fun validateInput(uiState: ItemDetails = itemUiState.itemDetails): Boolean { + private fun validateInput(uiState: TaskDetails = itemUiState.taskDetails): Boolean { return with(uiState) { - name.isNotBlank() && price.isNotBlank() && quantity.isNotBlank() + name.isNotBlank() && priority.isNotBlank() } } } diff --git a/app/src/main/java/com/example/inventory/ui/item/ItemEntryScreen.kt b/app/src/main/java/com/example/inventory/ui/task/TaskEntryScreen.kt similarity index 69% rename from app/src/main/java/com/example/inventory/ui/item/ItemEntryScreen.kt rename to app/src/main/java/com/example/inventory/ui/task/TaskEntryScreen.kt index e542de8c..619295b9 100644 --- a/app/src/main/java/com/example/inventory/ui/item/ItemEntryScreen.kt +++ b/app/src/main/java/com/example/inventory/ui/task/TaskEntryScreen.kt @@ -14,15 +14,19 @@ * limitations under the License. */ -package com.example.inventory.ui.item +package com.example.inventory.ui.task import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button @@ -30,14 +34,18 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.viewmodel.compose.viewModel @@ -47,12 +55,10 @@ 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 object ItemEntryDestination : NavigationDestination { override val route = "item_entry" - override val titleRes = R.string.item_entry_title + override val titleRes = R.string.task_entry_title } @OptIn(ExperimentalMaterial3Api::class) @@ -101,7 +107,7 @@ fun ItemEntryScreen( @Composable fun ItemEntryBody( itemUiState: ItemUiState, - onItemValueChange: (ItemDetails) -> Unit, + onItemValueChange: (TaskDetails) -> Unit, onSaveClick: () -> Unit, modifier: Modifier = Modifier ) { @@ -110,7 +116,7 @@ fun ItemEntryBody( verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.padding_large)) ) { ItemInputForm( - itemDetails = itemUiState.itemDetails, + taskDetails = itemUiState.taskDetails, onValueChange = onItemValueChange, modifier = Modifier.fillMaxWidth() ) @@ -127,48 +133,22 @@ fun ItemEntryBody( @Composable fun ItemInputForm( - itemDetails: ItemDetails, + taskDetails: TaskDetails, modifier: Modifier = Modifier, - onValueChange: (ItemDetails) -> Unit = {}, + onValueChange: (TaskDetails) -> Unit = {}, enabled: Boolean = true + ) { + val priorities = listOf("High", "Medium", "Low") + 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)) }, + value = taskDetails.name, + onValueChange = { onValueChange(taskDetails.copy(name = it)) }, + label = { Text(stringResource(R.string.task_name_req)) }, colors = OutlinedTextFieldDefaults.colors( focusedContainerColor = MaterialTheme.colorScheme.secondaryContainer, unfocusedContainerColor = MaterialTheme.colorScheme.secondaryContainer, @@ -178,11 +158,41 @@ fun ItemInputForm( enabled = enabled, singleLine = true ) - if (enabled) { - Text( - text = stringResource(R.string.required_fields), - modifier = Modifier.padding(start = dimensionResource(id = R.dimen.padding_medium)) - ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = dimensionResource(id = R.dimen.padding_small)), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + priorities.forEach { level -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .weight(1f) + .selectable( + selected = (taskDetails.priority == level), + onClick = { onValueChange(taskDetails.copy(priority = level)) }, + role = Role.RadioButton + ) + ) { + RadioButton( + selected = (taskDetails.priority == level), + onClick = null, // 整個 Row 都可點 + enabled = enabled + ) + Spacer(modifier = Modifier.width(dimensionResource(id = R.dimen.padding_small))) + Text(level, + color = if (level.lowercase() == "high") + MaterialTheme.colorScheme.error //High Red + else if (level.lowercase() == "medium") + Color(0xFFFBC02D) //High Yellow + else + Color(0xFF388E3C) //High Green + + ) + } + } } } } @@ -192,8 +202,8 @@ fun ItemInputForm( private fun ItemEntryScreenPreview() { InventoryTheme { ItemEntryBody(itemUiState = ItemUiState( - ItemDetails( - name = "Item name", price = "10.00", quantity = "5" + TaskDetails( + name = "Task name", priority = "LOW" ) ), onItemValueChange = {}, onSaveClick = {}) } diff --git a/app/src/main/java/com/example/inventory/ui/item/ItemEntryViewModel.kt b/app/src/main/java/com/example/inventory/ui/task/TaskEntryViewModel.kt similarity index 56% rename from app/src/main/java/com/example/inventory/ui/item/ItemEntryViewModel.kt rename to app/src/main/java/com/example/inventory/ui/task/TaskEntryViewModel.kt index 9729464c..2910ca94 100644 --- a/app/src/main/java/com/example/inventory/ui/item/ItemEntryViewModel.kt +++ b/app/src/main/java/com/example/inventory/ui/task/TaskEntryViewModel.kt @@ -14,20 +14,20 @@ * limitations under the License. */ -package com.example.inventory.ui.item +package com.example.inventory.ui.task import androidx.compose.runtime.getValue 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 com.example.inventory.data.Task +import com.example.inventory.data.TasksRepository import java.text.NumberFormat /** * ViewModel to validate and insert items in the Room database. */ -class ItemEntryViewModel(private val itemsRepository: ItemsRepository) : ViewModel() { +class ItemEntryViewModel(private val tasksRepository: TasksRepository) : ViewModel() { /** * Holds current item ui state @@ -39,23 +39,23 @@ class ItemEntryViewModel(private val itemsRepository: ItemsRepository) : ViewMod * Updates the [itemUiState] with the value provided in the argument. This method also triggers * a validation for input values. */ - fun updateUiState(itemDetails: ItemDetails) { + fun updateUiState(taskDetails: TaskDetails) { itemUiState = - ItemUiState(itemDetails = itemDetails, isEntryValid = validateInput(itemDetails)) + ItemUiState(taskDetails = taskDetails, isEntryValid = validateInput(taskDetails)) } /** - * Inserts an [Item] in the Room database + * Inserts an [Task] in the Room database */ suspend fun saveItem() { if (validateInput()) { - itemsRepository.insertItem(itemUiState.itemDetails.toItem()) + tasksRepository.insertTask(itemUiState.taskDetails.toTask()) } } - private fun validateInput(uiState: ItemDetails = itemUiState.itemDetails): Boolean { + private fun validateInput(uiState: TaskDetails = itemUiState.taskDetails): Boolean { return with(uiState) { - name.isNotBlank() && price.isNotBlank() && quantity.isNotBlank() + name.isNotBlank() && priority.isNotBlank() } } } @@ -64,47 +64,41 @@ class ItemEntryViewModel(private val itemsRepository: ItemsRepository) : ViewMod * Represents Ui State for an Item. */ data class ItemUiState( - val itemDetails: ItemDetails = ItemDetails(), + val taskDetails: TaskDetails = TaskDetails(), val isEntryValid: Boolean = false ) -data class ItemDetails( +data class TaskDetails( val id: Int = 0, val name: String = "", - val price: String = "", - val quantity: String = "", + val priority: String = "", ) /** - * Extension function to convert [ItemUiState] to [Item]. If the value of [ItemDetails.price] is + * Extension function to convert [ItemUiState] to [Task]. If the value of [TaskDetails.price] is * not a valid [Double], then the price will be set to 0.0. Similarly if the value of * [ItemUiState] is not a valid [Int], then the quantity will be set to 0 */ -fun ItemDetails.toItem(): Item = Item( +fun TaskDetails.toTask(): Task = Task( id = id, name = name, - price = price.toDoubleOrNull() ?: 0.0, - quantity = quantity.toIntOrNull() ?: 0 + priority = priority ) -fun Item.formatedPrice(): String { - return NumberFormat.getCurrencyInstance().format(price) -} /** - * Extension function to convert [Item] to [ItemUiState] + * Extension function to convert [Task] to [ItemUiState] */ -fun Item.toItemUiState(isEntryValid: Boolean = false): ItemUiState = ItemUiState( - itemDetails = this.toItemDetails(), +fun Task.toItemUiState(isEntryValid: Boolean = false): ItemUiState = ItemUiState( + taskDetails = this.toItemDetails(), isEntryValid = isEntryValid ) /** - * Extension function to convert [Item] to [ItemDetails] + * Extension function to convert [Task] to [TaskDetails] */ -fun Item.toItemDetails(): ItemDetails = ItemDetails( +fun Task.toItemDetails(): TaskDetails = TaskDetails( id = id, name = name, - price = price.toString(), - quantity = quantity.toString() + priority = priority ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 66262ff3..5eeb8d99 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,25 +14,26 @@ ~ limitations under the License. --> - Inventory + To Do List Attention Back Delete + Edit Are you sure you want to delete? - Oops!\nNo items in the inventory.\nTap + to add. - Edit Item - Item - Item Details - Add Item - Item Name* - Item Price* + Oops!\nNo tasks in the lists.\nTap + to add. + Edit Task + Task + Task Details + Add Task + Task Name* No - Price - Quantity in stock - %d in stock - Quantity in Stock* + Priority level + Priority %s + Priority level* Save Sell Yes *required fields + Cancel + confirm diff --git a/build.gradle.kts b/build.gradle.kts index bc180f53..656cba28 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,8 +22,8 @@ buildscript { } plugins { - id("com.android.application") version "8.1.4" apply false - id("com.android.library") version "8.1.4" apply false + id("com.android.application") version "8.8.2" apply false + id("com.android.library") version "8.8.2" 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 }