Fix: Handle missing 'color', 'isArchived' and 'value' fields in DTOs and mappers to prevent JsonDataException
This commit is contained in:
59
feature/inventory/build.gradle.kts
Normal file
59
feature/inventory/build.gradle.kts
Normal file
@@ -0,0 +1,59 @@
|
||||
// [FILE] feature/inventory/build.gradle.kts
|
||||
// [SEMANTICS] build, dependencies
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("kotlin-kapt")
|
||||
id("dagger.hilt.android.plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.homebox.lens.feature.inventory"
|
||||
compileSdk = Versions.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdk = Versions.minSdk
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = Versions.composeCompiler
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":domain"))
|
||||
implementation(project(":ui"))
|
||||
|
||||
// AndroidX & Lifecycle
|
||||
implementation(Libs.coreKtx)
|
||||
implementation(Libs.lifecycleRuntime)
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
|
||||
|
||||
// Compose
|
||||
implementation(platform(Libs.composeBom))
|
||||
implementation(Libs.composeUi)
|
||||
implementation(Libs.composeUiGraphics)
|
||||
implementation(Libs.composeUiToolingPreview)
|
||||
implementation(Libs.composeMaterial3)
|
||||
implementation(Libs.navigationCompose)
|
||||
implementation(Libs.hiltNavigationCompose)
|
||||
|
||||
// Hilt
|
||||
implementation(Libs.hiltAndroid)
|
||||
kapt(Libs.hiltCompiler)
|
||||
|
||||
// Other
|
||||
implementation(Libs.timber)
|
||||
implementation("io.coil-kt:coil-compose:2.4.0")
|
||||
}
|
||||
// [END_FILE_feature/inventory/build.gradle.kts]
|
||||
@@ -0,0 +1,160 @@
|
||||
// [FILE] InventoryScreen.kt
|
||||
// [SEMANTICS] ui, screen, list, compose
|
||||
package com.homebox.lens.feature.inventory.ui
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.homebox.lens.domain.model.ItemSummary
|
||||
import com.homebox.lens.navigation.NavigationActions
|
||||
import com.homebox.lens.ui.navigation.Screen
|
||||
import com.homebox.lens.ui.R // Import R from ui module
|
||||
import com.homebox.lens.ui.common.MainScaffold
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Function('InventoryScreen')]
|
||||
// [RELATION: Function('InventoryScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
||||
// [RELATION: Function('InventoryScreen')] -> [CALLS] -> [Function('MainScaffold')]
|
||||
// [RELATION: Function('InventoryScreen')] -> [CONSUMES_STATE] -> [ViewModel('InventoryViewModel')]
|
||||
/**
|
||||
* @summary Composable function for the "Inventory" screen.
|
||||
* @param currentRoute The current route to highlight the active item in the Drawer.
|
||||
* @param navigationActions The object with navigation actions.
|
||||
* @param viewModel The ViewModel for this screen.
|
||||
*/
|
||||
@Composable
|
||||
fun InventoryScreen(
|
||||
currentRoute: String?,
|
||||
navigationActions: NavigationActions,
|
||||
viewModel: InventoryViewModel = hiltViewModel()
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
MainScaffold(
|
||||
topBarTitle = stringResource(id = R.string.inventory_list_title),
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions
|
||||
) { innerPadding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
when (val state = uiState) {
|
||||
is InventoryUiState.Loading -> CircularProgressIndicator()
|
||||
is InventoryUiState.Success -> InventoryList(
|
||||
items = state.items,
|
||||
onItemClick = { itemId ->
|
||||
navigationActions.navigateToItemDetails(itemId)
|
||||
}
|
||||
)
|
||||
is InventoryUiState.Error -> Text(text = state.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('InventoryScreen')]
|
||||
|
||||
// [ENTITY: Function('InventoryList')]
|
||||
// [RELATION: Function('InventoryList')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
|
||||
// [RELATION: Function('InventoryList')] -> [CALLS] -> [Function('ItemRow')]
|
||||
/**
|
||||
* @summary Displays a list of inventory items.
|
||||
* @param items The list of items to display.
|
||||
* @param onItemClick Callback invoked when an item is clicked.
|
||||
*/
|
||||
@Composable
|
||||
private fun InventoryList(
|
||||
items: List<ItemSummary>,
|
||||
onItemClick: (String) -> Unit
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(8.dp)
|
||||
) {
|
||||
items(items) { item ->
|
||||
ItemRow(item = item, onItemClick = onItemClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('InventoryList')]
|
||||
|
||||
// [ENTITY: Function('ItemRow')]
|
||||
// [RELATION: Function('ItemRow')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
|
||||
/**
|
||||
* @summary Displays a single row in the inventory list.
|
||||
* @param item The item to display.
|
||||
* @param onItemClick Callback invoked when the row is clicked.
|
||||
*/
|
||||
@Composable
|
||||
private fun ItemRow(
|
||||
item: ItemSummary,
|
||||
onItemClick: (String) -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
.clickable { onItemClick(item.id) },
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(item.image?.path)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
placeholder = painterResource(R.drawable.ic_placeholder),
|
||||
error = painterResource(R.drawable.ic_placeholder),
|
||||
contentDescription = item.name,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(64.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = item.name,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
item.location?.let {
|
||||
Text(
|
||||
text = it.name,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
// [AI_NOTE]: Quantity is not present in ItemSummary, so it's omitted for now.
|
||||
// Placeholder: quantity will be added later when ItemSummary is updated to include it.
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('ItemRow')]
|
||||
|
||||
// [END_FILE_InventoryScreen.kt]
|
||||
@@ -0,0 +1,40 @@
|
||||
// [FILE] InventoryUiState.kt
|
||||
// [SEMANTICS] ui, state_management, sealed_state
|
||||
package com.homebox.lens.feature.inventory.ui
|
||||
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.domain.model.ItemSummary
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: SealedInterface('InventoryUiState')]
|
||||
/**
|
||||
* @summary Represents the different states of the Inventory screen.
|
||||
* @invariant Only one state can be active at a time.
|
||||
*/
|
||||
sealed interface InventoryUiState {
|
||||
// [END_ENTITY: SealedInterface('InventoryUiState')]
|
||||
|
||||
// [ENTITY: Object('Loading')]
|
||||
/**
|
||||
* @summary Represents the loading state, where the list of items is being fetched.
|
||||
*/
|
||||
object Loading : InventoryUiState
|
||||
// [END_ENTITY: Object('Loading')]
|
||||
|
||||
// [ENTITY: DataClass('Success')]
|
||||
/**
|
||||
* @summary Represents the success state, where the list of items is available.
|
||||
* @param items The list of inventory items.
|
||||
*/
|
||||
data class Success(val items: List<ItemSummary>) : InventoryUiState
|
||||
// [END_ENTITY: DataClass('Success')]
|
||||
|
||||
// [ENTITY: DataClass('Error')]
|
||||
/**
|
||||
* @summary Represents the error state, where an error occurred while fetching items.
|
||||
* @param message The error message.
|
||||
*/
|
||||
data class Error(val message: String) : InventoryUiState
|
||||
// [END_ENTITY: DataClass('Error')]
|
||||
}
|
||||
// [END_FILE_InventoryUiState.kt]
|
||||
@@ -0,0 +1,77 @@
|
||||
// [FILE] InventoryViewModel.kt
|
||||
// [SEMANTICS] ui, viewmodel, state_management
|
||||
package com.homebox.lens.feature.inventory.ui
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.homebox.lens.domain.usecase.SearchItemsUseCase
|
||||
import com.homebox.lens.domain.model.PaginationResult
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import com.homebox.lens.domain.model.fold
|
||||
import javax.inject.Inject
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: ViewModel('InventoryViewModel')]
|
||||
// [RELATION: ViewModel('InventoryViewModel')] -> [DEPENDS_ON] -> [UseCase('SearchItemsUseCase')]
|
||||
// [RELATION: ViewModel('InventoryViewModel')] -> [EMITS_STATE] -> [SealedInterface('InventoryUiState')]
|
||||
/**
|
||||
* @summary ViewModel for the Inventory screen.
|
||||
* @param searchItemsUseCase The use case to search for inventory items.
|
||||
* @invariant The ViewModel manages the UI state and business logic for the inventory screen.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class InventoryViewModel @Inject constructor(
|
||||
private val searchItemsUseCase: SearchItemsUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
// [ENTITY: DataStructure('uiState')]
|
||||
private val _uiState = MutableStateFlow<InventoryUiState>(InventoryUiState.Loading)
|
||||
val uiState: StateFlow<InventoryUiState> = _uiState.asStateFlow()
|
||||
// [END_ENTITY: DataStructure('uiState')]
|
||||
|
||||
init {
|
||||
fetchInventoryItems()
|
||||
}
|
||||
|
||||
// [ENTITY: Function('fetchInventoryItems')]
|
||||
// [RELATION: Function('fetchInventoryItems')] -> [CALLS] -> [UseCase('SearchItemsUseCase')]
|
||||
// [RELATION: Function('fetchInventoryItems')] -> [MODIFIES_STATE_OF] -> [DataStructure('uiState')]
|
||||
/**
|
||||
* @summary Fetches the list of inventory items.
|
||||
* @sideeffect Updates the `uiState` with the result of the operation.
|
||||
*/
|
||||
private fun fetchInventoryItems() {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = InventoryUiState.Loading
|
||||
Timber.d("[DEBUG][INVENTORY_VIEW_MODEL][FETCH_START] Fetching inventory items.")
|
||||
try {
|
||||
val result = searchItemsUseCase(query = "")
|
||||
|
||||
result.fold(
|
||||
onSuccess = { paginationResult ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val items = paginationResult.items as List<com.homebox.lens.domain.model.ItemSummary>
|
||||
_uiState.value = InventoryUiState.Success(items)
|
||||
Timber.i("[INFO][INVENTORY_VIEW_MODEL][FETCH_SUCCESS] Successfully fetched %d items.", items.size)
|
||||
},
|
||||
onFailure = { throwable: Exception ->
|
||||
_uiState.value = InventoryUiState.Error(throwable.message ?: "Unknown error")
|
||||
Timber.e(throwable, "[ERROR][INVENTORY_VIEW_MODEL][FETCH_FAILURE] Error fetching inventory items: %s", throwable.message)
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = InventoryUiState.Error(e.message ?: "An unexpected error occurred")
|
||||
Timber.e(e, "[ERROR][INVENTORY_VIEW_MODEL][FETCH_UNEXPECTED] Unexpected error fetching inventory items: %s", e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('fetchInventoryItems')]
|
||||
}
|
||||
// [END_ENTITY: ViewModel('InventoryViewModel')]
|
||||
// [END_FILE_InventoryViewModel.kt]
|
||||
Reference in New Issue
Block a user