Files
Sage/PROJECT_STRUCTURE.md
Dani 7be7b270e6 Initial commit: Sage Kitchen Management App v1.0.0
 Features implemented:
- Smart inventory tracking with Hive database
- Barcode scanning with auto-populated product info
- Multiple API fallbacks (Open Food Facts, UPCItemDB)
- Smart expiration date predictions by category
- Discord webhook notifications (persisted)
- Custom sage leaf vector icon
- Material Design 3 UI with sage green theme
- Privacy Policy & Terms of Service
- Local-first, privacy-focused architecture

🎨 UI/UX:
- Home dashboard with inventory stats
- Add Item screen with barcode integration
- Inventory list with expiration indicators
- Settings with persistent preferences
- About section with legal docs

🔧 Technical:
- Flutter 3.35.5 with Riverpod state management
- Hive 2.2.3 for local database
- Mobile scanner for barcode detection
- Feature-first architecture

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 13:54:21 -04:00

1068 lines
29 KiB
Markdown

# SAGE - PROJECT STRUCTURE & SETUP GUIDE
## Flutter Project Organization 🌿
**Last Updated:** October 3, 2025
**Flutter Version:** 3.x
**Dart Version:** 3.x
---
## 📁 COMPLETE FILE STRUCTURE
```
sage/
├── android/ # Android-specific files
│ ├── app/
│ │ ├── src/
│ │ │ └── main/
│ │ │ ├── AndroidManifest.xml
│ │ │ └── res/
│ │ └── build.gradle
│ └── build.gradle
├── ios/ # iOS-specific files
│ ├── Runner/
│ │ ├── Info.plist
│ │ └── Assets.xcassets/
│ └── Runner.xcworkspace/
├── lib/ # Main application code
│ │
│ ├── main.dart # App entry point
│ ├── app.dart # Root app widget
│ │
│ ├── core/ # Core utilities & constants
│ │ │
│ │ ├── constants/
│ │ │ ├── app_constants.dart
│ │ │ ├── api_constants.dart
│ │ │ ├── colors.dart
│ │ │ ├── text_styles.dart
│ │ │ └── dimensions.dart
│ │ │
│ │ ├── utils/
│ │ │ ├── date_utils.dart
│ │ │ ├── validators.dart
│ │ │ ├── formatters.dart
│ │ │ └── helpers.dart
│ │ │
│ │ ├── extensions/
│ │ │ ├── date_extensions.dart
│ │ │ ├── string_extensions.dart
│ │ │ ├── color_extensions.dart
│ │ │ └── context_extensions.dart
│ │ │
│ │ └── errors/
│ │ ├── exceptions.dart
│ │ └── failures.dart
│ │
│ ├── features/ # Feature modules (feature-first architecture)
│ │ │
│ │ ├── home/
│ │ │ ├── screens/
│ │ │ │ └── home_screen.dart
│ │ │ ├── widgets/
│ │ │ │ ├── expiring_soon_carousel.dart
│ │ │ │ ├── quick_stats_card.dart
│ │ │ │ └── quick_actions.dart
│ │ │ └── controllers/
│ │ │ └── home_controller.dart
│ │ │
│ │ ├── inventory/
│ │ │ ├── models/
│ │ │ │ ├── food_item.dart
│ │ │ │ ├── food_item.g.dart # Generated by Isar
│ │ │ │ └── location.dart
│ │ │ │
│ │ │ ├── repositories/
│ │ │ │ ├── inventory_repository.dart
│ │ │ │ └── inventory_repository_impl.dart
│ │ │ │
│ │ │ ├── controllers/
│ │ │ │ ├── inventory_controller.dart
│ │ │ │ └── item_form_controller.dart
│ │ │ │
│ │ │ ├── screens/
│ │ │ │ ├── inventory_screen.dart
│ │ │ │ ├── add_item_screen.dart
│ │ │ │ ├── edit_item_screen.dart
│ │ │ │ └── item_detail_screen.dart
│ │ │ │
│ │ │ └── widgets/
│ │ │ ├── inventory_list.dart
│ │ │ ├── inventory_item_card.dart
│ │ │ ├── expiration_badge.dart
│ │ │ ├── location_filter.dart
│ │ │ └── category_chip.dart
│ │ │
│ │ ├── barcode/
│ │ │ ├── screens/
│ │ │ │ └── barcode_scanner_screen.dart
│ │ │ ├── widgets/
│ │ │ │ ├── scanner_overlay.dart
│ │ │ │ └── product_info_card.dart
│ │ │ └── controllers/
│ │ │ └── barcode_controller.dart
│ │ │
│ │ ├── recipes/
│ │ │ ├── models/
│ │ │ │ ├── recipe.dart
│ │ │ │ ├── recipe.g.dart
│ │ │ │ ├── ingredient.dart
│ │ │ │ └── difficulty_level.dart
│ │ │ │
│ │ │ ├── repositories/
│ │ │ │ ├── recipe_repository.dart
│ │ │ │ └── recipe_repository_impl.dart
│ │ │ │
│ │ │ ├── controllers/
│ │ │ │ ├── recipe_controller.dart
│ │ │ │ ├── recipe_form_controller.dart
│ │ │ │ └── recipe_matcher_controller.dart
│ │ │ │
│ │ │ ├── screens/
│ │ │ │ ├── recipes_screen.dart
│ │ │ │ ├── add_recipe_screen.dart
│ │ │ │ ├── edit_recipe_screen.dart
│ │ │ │ ├── recipe_detail_screen.dart
│ │ │ │ ├── what_can_i_make_screen.dart
│ │ │ │ └── use_it_up_screen.dart
│ │ │ │
│ │ │ └── widgets/
│ │ │ ├── recipe_card.dart
│ │ │ ├── recipe_list.dart
│ │ │ ├── ingredient_list.dart
│ │ │ ├── recipe_tag_chip.dart
│ │ │ └── match_percentage_indicator.dart
│ │ │
│ │ ├── shopping/
│ │ │ ├── models/
│ │ │ │ ├── shopping_list.dart
│ │ │ │ ├── shopping_list.g.dart
│ │ │ │ ├── shopping_item.dart
│ │ │ │ └── priority.dart
│ │ │ │
│ │ │ ├── repositories/
│ │ │ │ ├── shopping_repository.dart
│ │ │ │ └── shopping_repository_impl.dart
│ │ │ │
│ │ │ ├── controllers/
│ │ │ │ ├── shopping_lists_controller.dart
│ │ │ │ └── shopping_list_controller.dart
│ │ │ │
│ │ │ ├── screens/
│ │ │ │ ├── shopping_lists_screen.dart
│ │ │ │ ├── list_detail_screen.dart
│ │ │ │ └── create_list_screen.dart
│ │ │ │
│ │ │ └── widgets/
│ │ │ ├── shopping_list_card.dart
│ │ │ ├── shopping_item_tile.dart
│ │ │ ├── list_progress_indicator.dart
│ │ │ └── quick_add_dialog.dart
│ │ │
│ │ ├── settings/
│ │ │ ├── models/
│ │ │ │ ├── user_settings.dart
│ │ │ │ ├── user_settings.g.dart
│ │ │ │ └── shopping_frequency.dart
│ │ │ │
│ │ │ ├── repositories/
│ │ │ │ ├── settings_repository.dart
│ │ │ │ └── settings_repository_impl.dart
│ │ │ │
│ │ │ ├── controllers/
│ │ │ │ ├── settings_controller.dart
│ │ │ │ └── theme_controller.dart
│ │ │ │
│ │ │ ├── screens/
│ │ │ │ ├── settings_screen.dart
│ │ │ │ ├── notifications_settings_screen.dart
│ │ │ │ ├── sync_settings_screen.dart
│ │ │ │ ├── discord_settings_screen.dart
│ │ │ │ └── shopping_frequency_screen.dart
│ │ │ │
│ │ │ └── widgets/
│ │ │ ├── settings_tile.dart
│ │ │ ├── settings_section.dart
│ │ │ └── frequency_picker.dart
│ │ │
│ │ └── auth/
│ │ ├── models/
│ │ │ └── user.dart
│ │ │
│ │ ├── repositories/
│ │ │ ├── auth_repository.dart
│ │ │ └── auth_repository_impl.dart
│ │ │
│ │ ├── controllers/
│ │ │ └── auth_controller.dart
│ │ │
│ │ └── screens/
│ │ ├── login_screen.dart
│ │ ├── signup_screen.dart
│ │ └── onboarding_screen.dart
│ │
│ ├── services/ # Business logic services
│ │ ├── notification_service.dart
│ │ ├── sync_service.dart
│ │ ├── barcode_service.dart
│ │ ├── discord_service.dart
│ │ ├── openfoodfacts_service.dart
│ │ ├── expiration_tracker_service.dart
│ │ ├── shopping_day_calculator.dart
│ │ └── recipe_parser_service.dart
│ │
│ ├── data/ # Data layer
│ │ ├── local/
│ │ │ ├── isar_database.dart
│ │ │ └── database_provider.dart
│ │ │
│ │ └── remote/
│ │ ├── supabase_client.dart
│ │ └── api_client.dart
│ │
│ └── shared/ # Shared components
│ ├── widgets/
│ │ ├── custom_button.dart
│ │ ├── custom_text_field.dart
│ │ ├── custom_dropdown.dart
│ │ ├── loading_indicator.dart
│ │ ├── error_widget.dart
│ │ ├── empty_state.dart
│ │ └── app_bar.dart
│ │
│ ├── navigation/
│ │ ├── app_router.dart
│ │ └── route_names.dart
│ │
│ └── providers/
│ └── common_providers.dart
├── assets/ # Static assets
│ ├── images/
│ │ ├── logo.png
│ │ ├── onboarding/
│ │ └── placeholders/
│ │
│ ├── icons/
│ │ └── app_icon.png
│ │
│ └── fonts/
│ └── # Custom fonts if needed
├── test/ # Tests
│ ├── unit/
│ │ ├── models/
│ │ ├── services/
│ │ └── repositories/
│ │
│ ├── widget/
│ │ └── widgets/
│ │
│ └── integration/
│ └── flows/
├── pubspec.yaml # Dependencies & assets
├── analysis_options.yaml # Linter rules
├── README.md # Project documentation
├── .gitignore # Git ignore rules
└── .env.example # Environment variables template
```
---
## 📦 PUBSPEC.YAML
Complete dependency file:
```yaml
name: sage
description: Smart Kitchen Management System
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
# State Management
flutter_riverpod: ^2.4.0
riverpod_annotation: ^2.3.0
# Database - Local
isar: ^3.1.0
isar_flutter_libs: ^3.1.0
path_provider: ^2.1.0
# Database - Cloud (Optional)
supabase_flutter: ^2.0.0
# Barcode Scanning
mobile_scanner: ^3.5.0
permission_handler: ^11.0.0
# HTTP & API
http: ^1.1.0
dio: ^5.4.0 # Alternative to http, better features
# Notifications
flutter_local_notifications: ^16.0.0
timezone: ^0.9.2
# Images
cached_network_image: ^3.3.0
image_picker: ^1.0.0
# UI Components
flutter_slidable: ^3.0.0 # Swipe actions
shimmer: ^3.0.0 # Loading placeholders
# Utilities
intl: ^0.18.0 # Date formatting, i18n
uuid: ^4.0.0 # Generate unique IDs
share_plus: ^7.2.0 # Share functionality
url_launcher: ^6.2.0 # Open URLs
# QR Codes (for sharing)
qr_flutter: ^4.1.0
qr_code_scanner: ^1.0.0
# Secure Storage
flutter_secure_storage: ^9.0.0
# Animations
animations: ^2.0.0
# Forms & Validation
flutter_form_builder: ^9.1.0
form_builder_validators: ^9.0.0
dev_dependencies:
flutter_test:
sdk: flutter
# Code Generation
isar_generator: ^3.1.0
build_runner: ^2.4.0
riverpod_generator: ^2.3.0
# Linting
flutter_lints: ^3.0.0
# Testing
mockito: ^5.4.0
integration_test:
sdk: flutter
flutter:
uses-material-design: true
assets:
- assets/images/
- assets/icons/
# fonts:
# - family: CustomFont
# fonts:
# - asset: assets/fonts/CustomFont-Regular.ttf
# - asset: assets/fonts/CustomFont-Bold.ttf
# weight: 700
```
---
## 🚀 SETUP INSTRUCTIONS
### Prerequisites
1. **Install Flutter:**
```bash
# macOS (using Homebrew)
brew install --cask flutter
# Windows - Download from:
# https://docs.flutter.dev/get-started/install/windows
# Linux
# Follow: https://docs.flutter.dev/get-started/install/linux
```
2. **Verify Installation:**
```bash
flutter doctor
```
Fix any issues reported (Android Studio, Xcode, etc.)
3. **Install Android Studio:**
- Download from: https://developer.android.com/studio
- Install Android SDK
- Set up Android emulator
---
### Project Creation
1. **Create Flutter Project:**
```bash
flutter create sage
cd sage
```
2. **Update pubspec.yaml:**
- Replace with the pubspec.yaml content above
- Run: `flutter pub get`
3. **Generate Isar Files:**
```bash
flutter pub run build_runner build
```
4. **Set Up Git:**
```bash
git init
git add .
git commit -m "Initial commit"
```
---
### Android Configuration
#### 1. Update AndroidManifest.xml
**File:** `android/app/src/main/AndroidManifest.xml`
```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<!-- Camera feature (optional, won't block non-camera devices) -->
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<application
android:label="Sage"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
```
#### 2. Update build.gradle (app level)
**File:** `android/app/build.gradle`
```gradle
android {
namespace "com.sage.app"
compileSdkVersion 34
defaultConfig {
applicationId "com.sage.app"
minSdkVersion 21 // Minimum for most packages
targetSdkVersion 34
versionCode 1
versionName "1.0.0"
multiDexEnabled true
}
buildTypes {
release {
signingConfig signingConfigs.debug // Change for production
minifyEnabled true
shrinkResources true
}
}
}
dependencies {
implementation 'androidx.multidex:multidex:2.0.1'
}
```
---
### iOS Configuration
#### 1. Update Info.plist
**File:** `ios/Runner/Info.plist`
```xml
<dict>
<!-- ... existing entries ... -->
<!-- Camera Permission -->
<key>NSCameraUsageDescription</key>
<string>Sage needs camera access to scan barcodes for quick item entry.</string>
<!-- Photo Library (if using image_picker) -->
<key>NSPhotoLibraryUsageDescription</key>
<string>Sage needs access to your photos to attach images to items.</string>
<!-- Notifications -->
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>
</dict>
```
#### 2. Update minimum iOS version
**File:** `ios/Podfile`
```ruby
platform :ios, '12.0' # Minimum for most packages
```
---
## 🏗️ CODE ORGANIZATION PATTERNS
### Feature-First Architecture
Each feature is self-contained with its own:
- Models (data structures)
- Repositories (data access)
- Controllers (business logic)
- Screens (UI)
- Widgets (reusable UI components)
**Benefits:**
- Easy to find related code
- Easy to test in isolation
- Easy to add/remove features
- Clear separation of concerns
---
### Riverpod Providers Structure
**File:** `lib/features/inventory/controllers/inventory_controller.dart`
```dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/food_item.dart';
import '../repositories/inventory_repository.dart';
// Provider for repository
final inventoryRepositoryProvider = Provider<InventoryRepository>((ref) {
return InventoryRepositoryImpl();
});
// Provider for controller
final inventoryControllerProvider =
StateNotifierProvider<InventoryController, AsyncValue<List<FoodItem>>>((ref) {
final repository = ref.watch(inventoryRepositoryProvider);
return InventoryController(repository);
});
// Controller class
class InventoryController extends StateNotifier<AsyncValue<List<FoodItem>>> {
final InventoryRepository _repository;
InventoryController(this._repository) : super(const AsyncValue.loading()) {
loadItems();
}
Future<void> loadItems() async {
state = const AsyncValue.loading();
try {
final items = await _repository.getAllItems();
state = AsyncValue.data(items);
} catch (e, stack) {
state = AsyncValue.error(e, stack);
}
}
Future<void> addItem(FoodItem item) async {
await _repository.addItem(item);
await loadItems(); // Refresh list
}
Future<void> deleteItem(int id) async {
await _repository.deleteItem(id);
await loadItems();
}
}
```
---
### Repository Pattern
**File:** `lib/features/inventory/repositories/inventory_repository.dart`
```dart
import '../models/food_item.dart';
abstract class InventoryRepository {
Future<List<FoodItem>> getAllItems();
Future<FoodItem?> getItemById(int id);
Future<void> addItem(FoodItem item);
Future<void> updateItem(FoodItem item);
Future<void> deleteItem(int id);
Future<List<FoodItem>> getItemsExpiringWithinDays(int days);
Future<List<FoodItem>> getItemsByLocation(Location location);
}
```
**File:** `lib/features/inventory/repositories/inventory_repository_impl.dart`
```dart
import 'package:isar/isar.dart';
import '../models/food_item.dart';
import 'inventory_repository.dart';
import '../../../data/local/isar_database.dart';
class InventoryRepositoryImpl implements InventoryRepository {
final Isar _isar = IsarDatabase.instance;
@override
Future<List<FoodItem>> getAllItems() async {
return await _isar.foodItems.where().findAll();
}
@override
Future<FoodItem?> getItemById(int id) async {
return await _isar.foodItems.get(id);
}
@override
Future<void> addItem(FoodItem item) async {
await _isar.writeTxn(() async {
await _isar.foodItems.put(item);
});
}
@override
Future<void> updateItem(FoodItem item) async {
await _isar.writeTxn(() async {
await _isar.foodItems.put(item);
});
}
@override
Future<void> deleteItem(int id) async {
await _isar.writeTxn(() async {
await _isar.foodItems.delete(id);
});
}
@override
Future<List<FoodItem>> getItemsExpiringWithinDays(int days) async {
final targetDate = DateTime.now().add(Duration(days: days));
return await _isar.foodItems
.filter()
.expirationDateLessThan(targetDate)
.findAll();
}
@override
Future<List<FoodItem>> getItemsByLocation(Location location) async {
return await _isar.foodItems
.filter()
.locationEqualTo(location)
.findAll();
}
}
```
---
### Service Layer
**File:** `lib/services/expiration_tracker_service.dart`
```dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../features/inventory/repositories/inventory_repository.dart';
import '../features/settings/repositories/settings_repository.dart';
import 'notification_service.dart';
import 'discord_service.dart';
final expirationTrackerProvider = Provider<ExpirationTrackerService>((ref) {
final inventory = ref.watch(inventoryRepositoryProvider);
final settings = ref.watch(settingsRepositoryProvider);
final notifications = ref.watch(notificationServiceProvider);
final discord = ref.watch(discordServiceProvider);
return ExpirationTrackerService(
inventory: inventory,
settings: settings,
notifications: notifications,
discord: discord,
);
});
class ExpirationTrackerService {
final InventoryRepository inventory;
final SettingsRepository settings;
final NotificationService notifications;
final DiscordService discord;
ExpirationTrackerService({
required this.inventory,
required this.settings,
required this.notifications,
required this.discord,
});
Future<void> runDailyCheck() async {
final userSettings = await settings.getSettings();
final allItems = await inventory.getAllItems();
for (final item in allItems) {
await _checkAndNotify(item, userSettings);
}
}
Future<void> _checkAndNotify(FoodItem item, UserSettings settings) async {
final days = item.daysUntilExpiration;
// 1 month notification
if (days == 30 && settings.notify1Month) {
await notifications.scheduleNotification(
title: '🌿 Sage: FYI',
body: '${item.name} expires in 30 days',
payload: 'item:${item.id}',
);
}
// 2 weeks notification
if (days == 14 && settings.notify2Weeks) {
await notifications.scheduleNotification(
title: '🌿 Sage: Heads up!',
body: '${item.name} expires in 2 weeks',
payload: 'item:${item.id}',
);
}
// 1 week notification
if (days == 7 && settings.notify1Week) {
await notifications.scheduleNotification(
title: '🌿 Sage: Use soon!',
body: '${item.name} expires in 1 week',
payload: 'useItUp:${item.id}',
);
}
// Shopping day check
if (settings.nextShoppingDay != null) {
if (item.expirationDate.isBefore(settings.nextShoppingDay!)) {
await notifications.scheduleNotification(
title: '🌿 Sage: Add to list!',
body: '${item.name} expires before shopping day',
payload: 'addToList:${item.id}',
);
// Discord alert for shopping day items
if (settings.enableDiscord) {
// Collect all such items and send once
}
}
}
}
}
```
---
## 🎨 UI COMPONENTS
### Screen Template
**File:** `lib/features/inventory/screens/inventory_screen.dart`
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../controllers/inventory_controller.dart';
import '../widgets/inventory_list.dart';
import '../../../shared/widgets/loading_indicator.dart';
import '../../../shared/widgets/error_widget.dart';
class InventoryScreen extends ConsumerWidget {
const InventoryScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final inventoryState = ref.watch(inventoryControllerProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Inventory'),
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
// Navigate to search
},
),
],
),
body: inventoryState.when(
data: (items) => items.isEmpty
? const EmptyState(
message: 'No items yet!\nTap + to add your first item.',
)
: InventoryList(items: items),
loading: () => const LoadingIndicator(),
error: (error, stack) => CustomErrorWidget(
error: error.toString(),
onRetry: () => ref.refresh(inventoryControllerProvider),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Navigate to add item screen
},
child: const Icon(Icons.add),
),
);
}
}
```
---
## 🧪 TESTING SETUP
### Unit Test Example
**File:** `test/unit/services/expiration_tracker_service_test.dart`
```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'package:sage/services/expiration_tracker_service.dart';
import 'package:sage/features/inventory/models/food_item.dart';
@GenerateMocks([InventoryRepository, SettingsRepository, NotificationService])
import 'expiration_tracker_service_test.mocks.dart';
void main() {
group('ExpirationTrackerService', () {
late ExpirationTrackerService service;
late MockInventoryRepository mockInventory;
late MockSettingsRepository mockSettings;
late MockNotificationService mockNotifications;
setUp(() {
mockInventory = MockInventoryRepository();
mockSettings = MockSettingsRepository();
mockNotifications = MockNotificationService();
service = ExpirationTrackerService(
inventory: mockInventory,
settings: mockSettings,
notifications: mockNotifications,
discord: MockDiscordService(),
);
});
test('sends notification for item expiring in 7 days', () async {
// Arrange
final item = FoodItem()
..name = 'Milk'
..expirationDate = DateTime.now().add(Duration(days: 7));
when(mockInventory.getAllItems()).thenAnswer((_) async => [item]);
when(mockSettings.getSettings()).thenAnswer((_) async => UserSettings());
// Act
await service.runDailyCheck();
// Assert
verify(mockNotifications.scheduleNotification(
title: '🌿 Sage: Use soon!',
body: 'Milk expires in 1 week',
payload: any,
)).called(1);
});
});
}
```
---
## 📝 CODE STYLE GUIDE
### Naming Conventions
```dart
// Classes: PascalCase
class FoodItem {}
class InventoryController {}
// Files: snake_case
// food_item.dart
// inventory_controller.dart
// Variables & functions: camelCase
final expirationDate = DateTime.now();
void loadItems() {}
// Constants: lowerCamelCase or UPPER_SNAKE_CASE for compile-time constants
const apiBaseUrl = 'https://api.sage.app';
const int MAX_ITEMS = 1000;
// Private members: prefix with _
final _repository = InventoryRepository();
void _internalMethod() {}
```
### Code Formatting
Run before committing:
```bash
# Format all Dart files
dart format .
# Analyze code for issues
flutter analyze
# Run tests
flutter test
```
---
## 🚀 DEVELOPMENT WORKFLOW
### 1. Start Development
```bash
# Get dependencies
flutter pub get
# Generate code (Isar, Riverpod)
flutter pub run build_runner watch
# Run app
flutter run
```
### 2. Development Mode
```bash
# Hot reload: Press 'r' in terminal
# Hot restart: Press 'R'
# Open DevTools: Press 'w'
```
### 3. Testing
```bash
# Run all tests
flutter test
# Run specific test file
flutter test test/unit/services/expiration_tracker_service_test.dart
# Run with coverage
flutter test --coverage
```
### 4. Building
```bash
# Android APK (debug)
flutter build apk --debug
# Android APK (release)
flutter build apk --release
# Android App Bundle (for Play Store)
flutter build appbundle --release
# iOS (requires Mac)
flutter build ios --release
```
---
## 📚 HELPFUL RESOURCES
### Official Documentation
- Flutter Docs: https://docs.flutter.dev/
- Dart Docs: https://dart.dev/guides
- Riverpod: https://riverpod.dev/
- Isar: https://isar.dev/
### Tutorials
- Flutter Codelabs: https://docs.flutter.dev/codelabs
- Riverpod Tutorial: https://codewithandrea.com/articles/flutter-state-management-riverpod/
- Isar Tutorial: https://isar.dev/tutorials/quickstart.html
### Community
- Flutter Discord: https://discord.gg/flutter
- r/FlutterDev: https://reddit.com/r/FlutterDev
- Stack Overflow: [flutter] tag
---
## ✅ PHASE 1 CHECKLIST
Use this to track Phase 1 progress:
- [ ] Flutter installed and verified (`flutter doctor`)
- [ ] Android Studio set up with emulator
- [ ] Project created (`flutter create sage`)
- [ ] Dependencies added to pubspec.yaml
- [ ] Run `flutter pub get`
- [ ] Isar code generation working (`build_runner`)
- [ ] Project structure created (folders)
- [ ] Basic models created (FoodItem, etc.)
- [ ] Repository pattern implemented
- [ ] Riverpod providers set up
- [ ] Home screen UI built
- [ ] Add Item screen (manual entry) built
- [ ] Inventory list screen built
- [ ] Local database working (save/load items)
- [ ] App runs on emulator/device
- [ ] Basic navigation working
---
**Ready to start coding? Let's build Sage! 🌿🔥**