Technical Design: Digital Twin Polish
Technical design for completing item, grid, assignment, location, and deletion interactions in Homey's digital twin.
1. Overview
This document provides the technical specification for completing the Digital Twin feature. It covers the missing interactions identified in MVP-STATUS.adoc:
-
Item Detail Dialog - View complete item information
-
Item Editing - Modify existing items
-
Item Deletion - Remove items with confirmation
-
Quick Assign Dialog - Assign unassigned items to grid cells
-
Cell Detail Dialog - View/manage items in a grid cell (when multiple items exist)
-
Location Editing - Rename/reconfigure locations
-
Location Deletion - Remove locations with cascade warnings
2. Feature Specifications
2.1 Item Detail Dialog
Purpose
Display complete item information in a modal dialog when user taps an item in grid or list view.
UI Design
+------------------------------------------+
| Item Details [X] |
+------------------------------------------+
| |
| [Item Photo or Category Icon] |
| |
| Name: Organic Walnuts |
| Brand: Nature's Best |
| Category: PANTRY |
| Quantity: 500g |
| Expiry: Jan 30, 2026 (7 days) |
| Location: Kitchen > Pantry > Shelf 2 |
| Position: Row 1, Col 3 |
| |
| +--------+ +--------+ +------------+ |
| | Edit | | Delete | | Move/Assign| |
| +--------+ +--------+ +------------+ |
| |
| [ Close ] |
+------------------------------------------+
Data Model
No new entities required. Uses existing GroceryItem domain model.
// New UI state for dialog
data class ItemDetailState(
val item: Item?,
val isVisible: Boolean = false,
val locationPath: String = "" // "Kitchen > Pantry > Shelf 2"
)
Interactions
| Trigger | Action | Result |
|---|---|---|
Tap item in grid cell |
Show ItemDetailDialog |
Dialog opens with item data |
Tap item in list view |
Show ItemDetailDialog |
Dialog opens with item data |
Tap "Edit" button |
Transition to Edit mode |
Dialog transforms to edit form |
Tap "Delete" button |
Show delete confirmation |
Confirmation dialog appears |
Tap "Move/Assign" button |
Show QuickAssignDialog |
Position picker appears |
Tap [X] or "Close" |
Dismiss dialog |
Dialog closes |
Tap outside dialog |
Dismiss dialog |
Dialog closes |
Expiry Visual Indicators
| Condition | Visual |
|---|---|
Expired |
Red background, "EXPIRED" badge |
Expires today |
Red text, "Expires today!" |
Expires in 1-3 days |
Orange text, "Expires in X days" |
Expires in 4-7 days |
Yellow text, "Expires in X days" |
Expires > 7 days |
Normal text, formatted date |
No expiry set |
Gray text, "No expiry date" |
2.2 Item Editing
UI Design
Two approaches considered:
Option A: In-Dialog Editing (Recommended) Transform the detail dialog into edit mode with editable fields.
+------------------------------------------+
| Edit Item [X] |
+------------------------------------------+
| |
| [Photo] [Change Photo] |
| |
| Name: [Organic Walnuts_____] |
| Brand: [Nature's Best_______] |
| Category: [ PANTRY v ] |
| Quantity: [500_] [g v] |
| Expiry: [Jan 30, 2026 ๐
] |
| |
| +--------+ +----------+ |
| | Cancel | | Save | |
| +--------+ +----------+ |
+------------------------------------------+
Option B: Separate Edit Screen Navigate to full-screen edit. More space but loses context.
Decision: Option A - In-dialog editing maintains context and is consistent with Add Item dialog.
Data Flow
User taps Edit
-> Dialog enters edit mode (isEditing = true)
-> Fields become editable
-> User modifies fields
-> User taps Save
-> Validate inputs
-> Call viewModel.updateItem(...)
-> Exit edit mode
-> Refresh item display
Validation Rules
| Field | Validation | Error Message |
|---|---|---|
Name |
Required, non-blank |
"Item name is required" |
Quantity |
Positive number |
"Quantity must be greater than 0" |
Expiry Date |
Future or today (warning if past) |
"This date is in the past" (warning, allow save) |
ViewModel Additions
// ItemViewModel.kt additions
fun updateItem(
itemId: String,
name: String,
category: ItemCategory,
brand: String?,
quantity: Double,
unit: String,
expiryDate: Date?
) {
viewModelScope.launch {
itemRepository.updateGroceryItem(
itemId = itemId,
name = name,
category = category.name,
brand = brand,
quantity = quantity,
unit = unit,
expiryDate = expiryDate
)
}
}
Repository Additions
// ItemRepository interface
suspend fun updateGroceryItem(
itemId: String,
name: String,
category: String,
brand: String?,
quantity: Double,
unit: String,
expiryDate: Date?
)
DAO Additions
// ItemDao.kt additions
@Query("""
UPDATE items
SET name = :name, category = :category
WHERE id = :itemId
""")
suspend fun updateItemBase(itemId: String, name: String, category: String)
@Query("""
UPDATE grocery_items
SET brand = :brand, quantity = :quantity, unit = :unit, expiryDate = :expiryDate
WHERE itemId = :itemId
""")
suspend fun updateGroceryDetails(
itemId: String,
brand: String?,
quantity: Double,
unit: String,
expiryDate: Date?
)
@Transaction
suspend fun updateGroceryItem(
itemId: String,
name: String,
category: String,
brand: String?,
quantity: Double,
unit: String,
expiryDate: Date?
) {
updateItemBase(itemId, name, category)
updateGroceryDetails(itemId, brand, quantity, unit, expiryDate)
}
2.3 Item Deletion
UI Design
+------------------------------------------+
| Delete Item? |
+------------------------------------------+
| |
| Are you sure you want to delete |
| "Organic Walnuts"? |
| |
| This action cannot be undone. |
| |
| +----------+ +-----------------+ |
| | Cancel | | Delete (Red) | |
| +----------+ +-----------------+ |
+------------------------------------------+
Interactions
| Trigger | Result |
|---|---|
User taps Delete in ItemDetailDialog |
Show DeleteConfirmationDialog |
User confirms deletion |
Item removed from DB, dialog closes, list updates |
User cancels |
Return to ItemDetailDialog |
ViewModel Additions
fun deleteItem(itemId: String) {
viewModelScope.launch {
itemRepository.deleteItem(itemId)
}
}
2.4 Quick Assign Dialog
UI Design
+------------------------------------------+
| Assign "Walnuts" to Grid [X] |
+------------------------------------------+
| |
| Select a cell to place this item: |
| |
| +-------+-------+-------+ |
| | [Milk]| [x3] | + | <- Row 1 |
| +-------+-------+-------+ |
| | + | + | [Oats]| <- Row 2 |
| +-------+-------+-------+ |
| | + | + | + | <- Row 3 |
| +-------+-------+-------+ |
| ^-- Empty cells show "+" |
| ^-- Occupied cells show item/count |
| |
| Tip: Tap an empty cell (+) to assign |
| Tap occupied cell to stack items |
| |
| [ Cancel ] |
+------------------------------------------+
Interactions
| Trigger | Action | Result |
|---|---|---|
Tap unassigned item chip |
Open QuickAssignDialog |
Grid picker appears |
Tap empty cell (+) |
Assign item to cell |
Item moves to cell, dialog closes |
Tap occupied cell |
Assign item (stack) |
Item added to cell, dialog closes |
Tap Cancel |
Dismiss |
Dialog closes, no change |
Component Specification
@Composable
fun QuickAssignDialog(
item: Item,
gridConfig: LocationGridConfig,
existingItems: Map<GridPosition, List<Item>>,
onAssign: (row: Int, column: Int) -> Unit,
onDismiss: () -> Unit
)
State Management
// Add to ItemViewState
data class ItemViewState(
// ... existing fields ...
val quickAssignItem: Item? = null // Item being assigned, null = dialog hidden
)
// ViewModel functions
fun startQuickAssign(item: Item) {
_viewState.value = _viewState.value.copy(quickAssignItem = item)
}
fun cancelQuickAssign() {
_viewState.value = _viewState.value.copy(quickAssignItem = null)
}
fun confirmQuickAssign(row: Int, column: Int) {
viewState.value.quickAssignItem?.let { item ->
assignItemToPosition(item.id.toString(), row, column)
}
cancelQuickAssign()
}
2.5 Cell Detail Dialog
UI Design
+------------------------------------------+
| Cell (Row 1, Col 2) [X] |
+------------------------------------------+
| |
| 3 items in this cell: |
| |
| +------------------------------------+ |
| | [icon] Coffee Beans | |
| | Brand X - Expires Feb 15 | |
| +------------------------------------+ |
| | [icon] Tea Bags | |
| | Lipton - No expiry | |
| +------------------------------------+ |
| | [icon] Hot Chocolate | |
| | Cadbury - Expires Mar 1 | |
| +------------------------------------+ |
| |
| Tap item for details |
| |
| [ Close ] |
+------------------------------------------+
Interactions
| Trigger | Action | Result |
|---|---|---|
Tap grid cell with multiple items |
Open CellDetailDialog |
List of items shown |
Tap individual item in list |
Open ItemDetailDialog |
Item details shown |
Tap Close |
Dismiss |
Dialog closes |
Component Specification
@Composable
fun CellDetailDialog(
row: Int,
column: Int,
items: List<Item>,
onItemClick: (Item) -> Unit,
onDismiss: () -> Unit
)
State Management
// Add to ItemViewState
data class ItemViewState(
// ... existing fields ...
val selectedCell: GridPosition? = null // null = no cell selected
)
fun selectCell(row: Int, column: Int) {
_viewState.value = _viewState.value.copy(
selectedCell = GridPosition(row, column)
)
}
fun clearCellSelection() {
_viewState.value = _viewState.value.copy(selectedCell = null)
}
2.6 Location Editing
UI Design
+------------------------------------------+
| Edit Location [X] |
+------------------------------------------+
| |
| Name: [Top Shelf______________] |
| |
| Parent: Kitchen > Pantry (readonly) |
| |
| --- Grid Settings --- |
| |
| Enable Grid: [X] |
| Rows: [===|====] 3 |
| Columns: [=====|==] 4 |
| |
| Preview: |
| +---+---+---+---+ |
| | | | | | |
| +---+---+---+---+ |
| | | | | | |
| +---+---+---+---+ |
| | | | | | |
| +---+---+---+---+ |
| |
| +----------+ +----------+ |
| | Cancel | | Save | |
| +----------+ +----------+ |
+------------------------------------------+
Access Points
-
Long-press location in LocationAccordion
-
"Edit" option in location context menu
-
Settings icon in ItemScreen top bar (for current location)
ViewModel Additions (LocationViewModel)
fun updateLocation(locationId: String, newName: String) {
viewModelScope.launch {
locationRepository.updateLocationName(locationId, newName)
}
}
DAO Additions
@Query("UPDATE locations SET name = :name WHERE id = :id")
suspend fun updateLocationName(id: String, name: String)
Grid Resize Logic
When grid dimensions are reduced: 1. Check if any items exist outside new bounds 2. If yes, show warning: "X items will become unassigned" 3. On confirm, set those items' gridRow/gridColumn to NULL
@Query("""
UPDATE items
SET gridRow = NULL, gridColumn = NULL
WHERE locationId = :locationId
AND (gridRow >= :newRows OR gridColumn >= :newColumns)
""")
suspend fun unassignItemsOutsideBounds(
locationId: String,
newRows: Int,
newColumns: Int
)
2.7 Location Deletion
UI Design
+------------------------------------------+
| Delete Location? |
+------------------------------------------+
| |
| Are you sure you want to delete |
| "Top Shelf"? |
| |
| WARNING: This will also delete: |
| - 12 items in this location |
| - 2 child locations |
| - Middle Shelf (8 items) |
| - Bottom Shelf (5 items) |
| |
| Total: 25 items will be deleted |
| |
| This action cannot be undone. |
| |
| +----------+ +------------------+ |
| | Cancel | | Delete All (Red) | |
| +----------+ +------------------+ |
+------------------------------------------+
Pre-Delete Analysis Query
data class LocationDeletionImpact(
val location: Location,
val directItemCount: Int,
val childLocations: List<LocationDeletionImpact>,
val totalItemCount: Int
)
// Repository function to analyze deletion impact
suspend fun analyzeDeletionImpact(locationId: String): LocationDeletionImpact
DAO Additions
@Query("SELECT COUNT(*) FROM items WHERE locationId = :locationId")
suspend fun getItemCountForLocation(locationId: String): Int
@Query("SELECT * FROM locations WHERE parentId = :parentId")
suspend fun getChildLocations(parentId: String): List<LocationEntity>
@Query("DELETE FROM locations WHERE id = :locationId")
suspend fun deleteLocation(locationId: String)
// Note: Items and children auto-deleted via CASCADE
3. State Management Summary
3.1 Updated ItemViewState
data class ItemViewState(
// Existing
val viewMode: ViewMode = ViewMode.LIST,
val searchQuery: String = "",
val selectedCategories: Set<ItemCategory> = emptySet(),
val sortBy: SortOption = SortOption.NAME,
val showUnassignedOnly: Boolean = false,
// New - Dialog states
val selectedItem: Item? = null, // For ItemDetailDialog
val isEditingItem: Boolean = false, // Edit mode in detail dialog
val quickAssignItem: Item? = null, // For QuickAssignDialog
val selectedCell: GridPosition? = null, // For CellDetailDialog
val itemToDelete: Item? = null // For delete confirmation
)
3.2 Updated LocationViewState
data class LocationViewState(
// Existing
val expandedLocationIds: Set<String> = emptySet(),
// New
val selectedLocation: Location? = null, // For editing
val locationToDelete: Location? = null, // For delete confirmation
val deletionImpact: LocationDeletionImpact? = null
)
4. Component Hierarchy
ItemScreen
โโโ TopAppBar (with view toggle, settings)
โโโ ItemSearchBar
โโโ Content Area
โ โโโ FlexibleGrid (grid mode)
โ โ โโโ GridCell
โ โ โโโ ItemCard (compact)
โ โโโ CategoryGroupList (list mode)
โ โโโ ItemCard (expanded)
โโโ UnassignedItemsBar
โ โโโ ItemChip
โโโ Dialogs
โโโ AddItemDialog (existing)
โโโ GridConfigDialog (existing)
โโโ ItemDetailDialog (NEW)
โ โโโ Edit mode transforms in-place
โโโ DeleteConfirmationDialog (NEW)
โโโ QuickAssignDialog (NEW)
โโโ CellDetailDialog (NEW)
LocationScreen
โโโ TopAppBar
โโโ LocationAccordion (recursive)
โ โโโ LocationRow
โ โโโ Context menu (edit, delete)
โโโ FAB (add location)
โโโ Dialogs
โโโ AddLocationDialog (existing)
โโโ EditLocationDialog (NEW)
โโโ DeleteLocationDialog (NEW)
5. Files to Create/Modify
New Files
| File | Purpose |
|---|---|
|
View/edit item details |
|
Generic delete confirmation |
|
Grid position picker |
|
Multi-item cell view |
|
Location editing |
|
Location deletion with impact |
Modified Files
| File | Changes |
|---|---|
|
Add update/delete functions, dialog state |
|
Wire up new dialogs, implement click handlers |
|
Add update/delete queries |
|
Add update/delete methods |
|
Implement new methods |
|
Add edit/delete functions |
|
Add update/delete queries |
|
Add update/delete/analyze methods |
|
Implement new methods |
|
Add edit/delete UI |
|
Add long-press context menu |
6. Implementation Order
Recommended sequence to minimize dependencies:
| Phase | Feature | Rationale |
|---|---|---|
1 |
Item Detail Dialog (view only) |
Foundation for other item interactions |
2 |
Item Deletion |
Simple, tests DAO deletion |
3 |
Item Editing |
Builds on detail dialog |
4 |
Quick Assign Dialog |
Enables core grid functionality |
5 |
Cell Detail Dialog |
Handles multi-item cells |
6 |
Location Editing |
Independent of item features |
7 |
Location Deletion |
Requires impact analysis |
7. Edge Cases & Error Handling
Item Operations
| Scenario | Handling | UI Feedback |
|---|---|---|
Edit item while offline |
Queue for sync (future) |
"Saved locally" |
Delete last item |
Show empty state |
"No items. Add one!" |
Assign to occupied cell |
Stack items |
Show count badge |
Grid disabled while items assigned |
Keep positions, hide grid |
Items visible in list |
Location Operations
| Scenario | Handling | UI Feedback |
|---|---|---|
Delete root with children |
Cascade delete all |
Show impact summary |
Rename to empty string |
Validation error |
"Name cannot be empty" |
Resize grid with items outside |
Unassign items |
"X items will be unassigned" |
Delete location with items in meal plan |
Future: warn |
N/A for MVP |
8. Testing Considerations
10. Open Questions
-
Drag-and-drop: Should we implement in this phase or defer?
-
Recommendation: Defer to Phase 2 - adds complexity, quick assign covers use case
-
-
Undo support: Should deletion be undoable?
-
Recommendation: Defer - would require soft delete, adds complexity
-
-
Batch operations: Multi-select for bulk delete/move?
-
Recommendation: Defer to Phase 2 - good feature but not MVP critical
-
11. Implementation Status
All features have been implemented:
| Feature | Status | Files Created/Modified |
|---|---|---|
Item Detail Dialog |
DONE |
|
Item Deletion |
DONE |
|
Item Editing |
DONE |
|
Quick Assign Dialog |
DONE |
|
Cell Detail Dialog |
DONE |
|
Location Editing |
DONE |
|
Location Deletion |
DONE |
|
Document Version: 1.1 Created: 2026-01-23 Updated: 2026-01-23 Status: IMPLEMENTED