Fix: Handle missing 'color', 'isArchived' and 'value' fields in DTOs and mappers to prevent JsonDataException

This commit is contained in:
2025-10-06 09:40:47 +03:00
parent 9500d747b1
commit 78b827f29e
31 changed files with 691 additions and 619 deletions

View 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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]