Shared Meal Planning Extraction
Design for evolving Homey's meal planning into a reusable, verified Gradle dependency.
Purpose
Homey already connects recipes, planned meals, household inventory, and weekly shopping needs. A separate infant meal-planning application needs the same recipe and quantity-reconciliation capabilities, but its daily schedule is age-driven: it can contain four or five feeding occurrences at regular intervals instead of Homey’s named breakfast, lunch, and dinner slots.
This design makes the common behaviour reusable without forcing either product to adopt the other’s terminology, schedule, UI, or storage model.
Why Refactor in Homey First
The shared library must be proven by a real consumer before it is extracted. Homey is that consumer: it has existing data, UI, recipe workflows, inventory matching, shopping persistence, and automated tests. Refactoring in place keeps the app buildable at every step and prevents an untested shared module from becoming a parallel implementation.
The infant application is represented initially by contract tests, not by adding infant age rules or screens to Homey. Those tests describe the capabilities the shared boundary must support: arbitrary daily occurrences, recipes used in more than one context, and quantity reconciliation with or without inventory.
Scope and Boundaries
| Concern | Shared capability | Host responsibility |
|---|---|---|
Schedule |
Arbitrary, ordered daily slot definitions and planned entries. |
Homey defines household meals; the infant app defines age-specific feeding schedules and intervals. |
Recipes |
Recipe, ingredient, step, serving-scaling, and generic tagging contracts. |
Each app decides which tags or attributes are appropriate for a selected slot. |
Shopping needs |
Aggregate recipe requirements and calculate the remaining quantity needed. |
Each app provides inventory, item matching, unit conversion policy, stores, persistence, and presentation. |
UI and navigation |
None in the first extraction. |
Each app owns its Compose screens, navigation, wording, and accessibility behaviour. |
Storage |
Repository contracts first; an optional shared Room implementation only after both apps need the same lifecycle. |
Each app owns its database integration and migrations. |
Current In-Place Refactor
The first completed slice removes Homey’s fixed meal enum from the core plan shape while preserving the existing Homey experience.
-
PlanSlotIdis a non-empty, host-defined identifier rather than a universal meal enum. -
DayMenuholds an arbitrary list of slots and offers lookup byPlanSlotId. -
MealSchedule<SlotId>orders any number of daily occurrences. -
Homey’s
MealTyperemains an adapter: its enum names are mapped to stable slot identifiers, so existing stored rows and the breakfast/lunch/dinner UI remain valid. -
Recipe insertion and lookup have generic slot-ID repository methods, with Homey
MealTypeoverloads retained for the existing screens. -
Stock-health and shopping inputs traverse every planned slot rather than a hard-coded breakfast/lunch/dinner/snack structure.
The Room column remains named mealType for compatibility, but it now stores
the host-defined plan-slot identifier. This is a source-only refactor: it does
not require a schema migration or transform existing user data.
Target Architecture
Homey adapters ────────┐
├── meal-domain (plan slots, recipes, quantities)
Infant app adapters ───┤
├── meal-engine (aggregation and shortage rules)
└── app-specific storage, policy, and Compose UI
The first published units should be pure Kotlin modules. They must not depend
on Android, Room, Hilt, Homey’s GroceryItem, Homey’s locations, or an infant
age policy. This keeps tests fast and lets each application choose its own
storage and UI lifecycle.
Implementation Plan
1. Generalize planning slots — complete
Replace fixed fields in the domain plan model with arbitrary slot IDs, while keeping Homey’s enum, routes, UI, and database values as adapters. Add tests for an ordered five-occurrence schedule and a five-slot day plan.
2. Separate recipe classification from schedule selection — complete
RecipeCategory currently includes Homey meal names. Introduce generic recipe
tags or attributes as the shared model. Keep Homey’s existing category and
selected-slot filtering as an adapter until the UI can be migrated without a
behaviour change. A recipe must be able to serve several schedule contexts.
3. Publish planning events — complete
After a plan write succeeds, publish immutable domain events such as
MealPlanned. The payload contains the plan slot, schedule time, recipe and
serving information, and generic ingredient amounts. It must never expose a
host inventory type such as Homey’s GroceryItem.
Each host provides its own event listener. Homey can use it to reconcile grocery inventory or shopping rows; the infant app can use the same event for its own records, notifications, or recommendations. Listener failures must not roll back a successful plan write. Hosts that need delivery guarantees own their durable outbox and retry policy.
4. Extract quantity reconciliation contracts — in progress
Move ingredient-requirement aggregation, unit-aware deficit calculation, and
ShoppingNeed output behind pure interfaces. Homey will adapt its
GroceryItem, aliases, consumption ratios, stores, and persisted weekly rows.
The infant app may provide a different inventory source or no inventory at all.
The shared model now owns Ingredient and IngredientStock. Homey converts
each GroceryItem to that model at its matching, stock-health, shopping, and
event-listener boundaries. That leaves Homey’s inventory identifiers,
normalization, aliases, quantities, locations, and persisted shopping rows
inside Homey while the reconciliation engine only sees ingredient stock.
MealPlanStockHealthCalculator now receives alias resolution and consumption
ratios through narrow contracts. Homey implements those contracts with its
existing Room data, while a host without learned consumption data uses the
default ratio of one. An infant-shaped test verifies an arbitrary feeding slot
and generic ingredient stock without constructing a Homey grocery item.
5. Stabilize the repository boundary — complete
Define small repository interfaces for recipes and plan entries, then make Homey’s Room implementation conform to them. Do not move Room tables merely to make a module compile; preserving existing user data takes priority.
As an extraction prerequisite, shared recipe and planning lifecycle enums live with their domain models. Homey still stores their unchanged enum names in its existing Room columns, so this source move has no schema or data migration.
RecipeRepository and MealSlotRepository now sit with the shared domain
contracts. They use generic planning tags and plan-slot IDs only. Homey’s
Room-backed implementations remain in the data layer; Homey meal names are
converted to their stable slot IDs at UI call sites. Its legacy recipe-category
column continues to receive the same Homey slot values, preserving existing
rows and auto-created meal recipes.
6. Extract and publish — in progress
After Homey has exercised the generic API and its contract tests pass, move the pure code into versioned Gradle modules. Homey first consumes them from a local composite build or included source checkout; both applications then consume a published Maven artifact with semantic versions.
The first extraction is now an included :meal-domain Kotlin module. Homey
depends on it directly and the original immutable plan, recipe, ingredient,
event, shopping, and stock model files were moved there without rewriting
their package names. Keeping the package source-compatible makes the module
move mechanically verifiable and avoids a behavioral change in the same
slice. The next extraction slice will introduce the neutral public package
before publishing the artifact for the infant application.
Verification and Commit Gates
Every slice must retain Homey’s existing behaviour and pass focused tests,
followed by testDebugUnitTest assembleDebug. Screen or navigation changes
also require the normal physical-device review before their commit. Extraction
does not begin until Homey’s in-place implementation and the infant-shaped
contract tests pass from the same source of truth.
Deliberate Non-Goals
-
Do not add infant schedules, age rules, feeding advice, or infant UI to Homey.
-
Do not share Compose screens in the first version.
-
Do not create a second Room database or migrate user data until both hosts demonstrably need a shared persistence lifecycle.
-
Do not treat a recipe’s use in a plan slot as a permanent recipe category.