Native Android meal planning app with AI-powered recipe generation.
A meal planning app for a single user on Android. The user selects 6 dinner recipes from AI-generated suggestions each week, generating a shopping list. After shopping, ingredients are added to the pantry. When a recipe is cooked, ingredients are auto-deducted from pantry.
Architecture: Native Kotlin (Jetpack Compose) + Node.js backend on Railway
Mealkit-Planner/
├── android-app/ # Native Android app (Kotlin + Jetpack Compose)
├── backend/ # Node.js/Express API (hosted on Railway)
├── public/data/ # Recipe dataset JSON (copied to backend)
├── scripts/ # Recipe import scripts
├── Todo.md # Current feature backlog
└── NATIVE_KOTLIN_PROGRESS.md # Kotlin rewrite progress tracker
The backend is deployed on Railway and auto-deploys from GitHub:
cd backend
npm install
cp ../public/data/recipes.json data/
npm run dev
# Runs on http://localhost:3001- Open
android-app/in Android Studio - Wait for Gradle sync
- For local backend: App uses
10.0.2.2:3001(emulator localhost) - Run on emulator or device
Native Kotlin with Jetpack Compose, following Clean Architecture:
app/src/main/java/com/mealplanner/
├── di/ # Hilt dependency injection modules
├── data/
│ ├── local/ # Room database, DAOs, entities
│ ├── remote/ # Retrofit APIs, DTOs
│ └── repository/ # Repository implementations
├── domain/ # Models, repository interfaces, use cases
├── presentation/
│ ├── components/ # Reusable UI components
│ ├── theme/ # Compose theme (colors, typography)
│ ├── navigation/ # NavGraph
│ └── screens/ # UI screens
└── service/ # Foreground service for AI generation
Key Technologies:
- Jetpack Compose (UI)
- Hilt (Dependency Injection)
- Room (Local Database)
- Retrofit (API Client)
- Coil (Image Loading)
Node.js/Express API providing:
- Recipe search from 20K+ Food.com dataset
- AI meal plan generation via Gemini (SSE streaming)
- Shopping list polishing via Gemini
- Pantry item categorization via Gemini (async job pattern)
- Image caching (planned)
Key AI endpoints:
POST /api/meal-plan/generate-async- AI meal plan generationPOST /api/meal-plan/polish-grocery-list-async- Grocery list cleanupPOST /api/meal-plan/categorize-pantry-items-async- AI pantry categorizationPOST /api/meal-plan/process-substitution- AI ingredient substitution
See backend/README.md for full API documentation.
Friday/Saturday: PLAN
┌────────────────────────────────────────────────────────────────────┐
│ 1. User taps "Generate Meal Plan" │
│ 2. Backend queries recipe dataset + Gemini AI │
│ 3. Presents 24 diverse meal options (SSE streaming progress) │
│ 4. User selects 6 favorites │
│ 5. Confirms → creates MealPlan + generates ShoppingList │
└────────────────────────────────────────────────────────────────────┘
│
▼
Weekend: SHOP
┌────────────────────────────────────────────────────────────────────┐
│ 1. User views shopping list (organized by store section) │
│ 2. "Shopping Mode" - tap items as found in store │
│ 3. All items checked → "Complete Shopping Trip" │
│ 4. Checked items auto-added to Pantry │
└────────────────────────────────────────────────────────────────────┘
│
▼
Weeknights: COOK
┌────────────────────────────────────────────────────────────────────┐
│ 1. User picks a recipe from meal plan │
│ 2. Views recipe detail (ingredients + steps) │
│ 3. Marks recipe as "Cooked" │
│ 4. App auto-deducts ingredients from pantry │
│ 5. User rates recipe (affects future recommendations) │
│ 6. 2 servings = dinner + leftover lunch │
└────────────────────────────────────────────────────────────────────┘
| Entity | Purpose |
|---|---|
PantryItem |
Pantry inventory with quantities, categories, expiry |
Recipe |
Saved recipes from selections |
MealPlan |
Weekly plans with recipe references |
DayPlan |
Individual day within a meal plan |
ShoppingItem |
Shopping list items with check state |
RecipeHistory |
Cooking history with ratings |
UserPreferences |
Taste profile (likes, dislikes) |
| Screen | Purpose |
|---|---|
HomeScreen |
Dashboard with week overview, quick stats, actions |
MealsScreen |
Generate/browse/select recipes, confirm meal plan |
ShoppingScreen |
Shopping list with shopping mode for in-store use |
PantryScreen |
Inventory management with water-level quantity cards |
RecipeDetailScreen |
Full recipe view, mark cooked, rate |
ProfileScreen |
Preferences, history, stats (tabbed) |
SettingsScreen |
API key config, preferences, test mode |
When "Done Shopping" is pressed in the Grocery List:
MealPlanScreen
└─> MealPlanViewModel.markShoppingComplete()
└─> Show confirmation screen with editable items
└─> User can edit quantities (e.g., bought 450g instead of 300g)
└─> User can edit names (substitutions: fresh corn → frozen corn)
└─> On confirm:
├─> Process substitutions via AI
│ ├─> POST /api/meal-plan/process-substitution
│ ├─> AI updates recipe name, quantities, preparation, instructions
│ └─> MealPlanRepository.updateRecipeWithSubstitution()
├─> UI shows "Stocking your pantry..." loading screen
└─> ManageShoppingListUseCase.completeShoppingTrip(mealPlanId)
├─> ShoppingRepository.getCheckedItems() - get all checked items
├─> ShoppingRepository.categorizeForPantry() - AI categorization
│ ├─> POST /api/meal-plan/categorize-pantry-items-async
│ ├─> Poll job status until complete
│ └─> Returns CategorizedPantryItem[] with smart categories
├─> (Fallback to local mapping if AI fails)
├─> Map to PantryItems with AI-determined:
│ ├─> Category (PRODUCE, PROTEIN, DAIRY, etc.)
│ ├─> TrackingStyle (STOCK_LEVEL, COUNT, PRECISE)
│ └─> ExpiryDays (based on item type)
└─> PantryRepository.addFromShoppingList() - insert to pantry
└─> MealPlanRepository.markShoppingComplete() - set flag
└─> Show confirmation dialog with item count
AI Ingredient Substitution (Gemini):
- Updates recipe name if ingredient is in the name (e.g., "Honey Garlic Salmon" → "Honey Garlic Tilapia")
- Converts quantities appropriately (e.g., fresh herbs → dried uses 1/3 amount)
- Updates preparation style (e.g., removes "torn" for dried herbs)
- Updates recipe cooking instructions to reference new ingredient
- Falls back to simple name update if AI unavailable
AI Categorization (Gemini):
- Intelligently assigns pantry categories (e.g., "Salmon Fillets" → PROTEIN)
- Determines tracking style (spices use STOCK_LEVEL, cans use COUNT)
- Estimates expiry days based on item perishability
- Falls back to local heuristics if AI unavailable
Key files:
ManageShoppingListUseCase.kt- orchestrates the pantry sync logicShoppingRepositoryImpl.kt- implements AI categorization with job pollingMealPlanViewModel.kt- calls use case, manages completion state, processes substitutionsMealPlanRepositoryImpl.kt- updates recipe JSON with substitution changesMealPlanScreen.kt- shows confirmation screen, loading screen, and completion dialogbackend/src/services/geminiService.ts-categorizePantryItems()andprocessSubstitution()functions
When a recipe is marked as "Cooked" via "I Made This!":
RecipeDetailScreen
└─> User taps "I Made This!" button
└─> RecipeDetailViewModel.startDeductionConfirmation(recipe)
└─> Matches recipe ingredients to pantry items (fuzzy matching)
└─> Creates PendingDeductionItem list with tracking style info
└─> Shows DeductionConfirmationScreen
│
├─> For STOCK_LEVEL items (spices, oils, condiments):
│ └─> 4-button selector: Out / Low / Some / Plenty
│ └─> Defaults to no change, user can adjust level
│
├─> For COUNT/PRECISE items (produce, proteins, cans):
│ └─> +/- buttons with half-unit support (0.5 increments)
│ └─> Shows "Used: X | Unused: Y" badge
│ └─> Long-press minus to skip item
│
└─> User confirms deductions
└─> RecipeDetailViewModel.confirmDeductions()
├─> PantryRepository.setStockLevel() for STOCK_LEVEL items
├─> PantryRepository.deductByName() for COUNT/PRECISE items
└─> markAsCooked() records history for rating
Key files:
PendingDeductionItem.kt- Data model for pending deductions with tracking style supportRecipeDetailViewModel.kt- Deduction confirmation flow logicRecipeDetailScreen.kt- Confirmation UI with adjustable deduction cards
Test Mode provides data isolation for testing pantry sync:
SettingsScreen
└─> SettingsViewModel.enableTestMode()
└─> TestModeUseCase.enableTestMode()
├─> clearAllData() - clears pantry, shopping, meal plans
└─> DataStore: test_mode_enabled = true
MainScreen shows "TEST MODE" banner when enabled
See NATIVE_KOTLIN_PROGRESS.md for detailed progress.
Completed:
- Native Kotlin app with all core screens
- Backend with recipe search and AI generation
- Foreground service for background generation
- Room database with full schema
- Shopping → Pantry flow with AI-powered categorization
- Confirmation screen before adding items to pantry (with editable quantities/names)
- AI-powered ingredient substitution (updates recipes intelligently)
- Recipe rating and history
- Test Mode for data isolation during development
- Cooking → Pantry deduction flow with confirmation screen
- Different UI for STOCK_LEVEL items (4-button level selector) vs COUNT/PRECISE items (+/- buttons)
- Half-unit support for quantities (0.5 increments)
- "Used: X | Unused: Y" badge display
- Long-press minus to skip items, auto-restore on adjustment
- Pantry adjusters with hold-to-repeat +/- buttons (speed scales with quantity magnitude)
In Progress (see Todo.md):
- Recipe units refinement
- Stock level tracking improvements
| Service | Purpose | Required? |
|---|---|---|
| Gemini | AI meal generation, recipe images | Yes (for AI features) |
Configure in Settings screen. Without Gemini:
- Can still browse/search recipes manually
- No AI-powered meal plan generation
-
Backend must be running for meal generation. Either:
- Use Railway deployment (production)
- Run locally with
npm run devinbackend/
-
Recipe dataset - 20K+ recipes from Food.com, stored in
backend/data/recipes.json -
Emulator networking - Android emulator uses
10.0.2.2to reach host localhost -
Foreground service - AI generation runs in a foreground service so it continues when app is backgrounded
# Debug build
cd android-app
./gradlew assembleDebug
# Release build (requires signing config)
./gradlew assembleReleaseAPK output: android-app/app/build/outputs/apk/