# 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 ``` #### 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 NSCameraUsageDescription Sage needs camera access to scan barcodes for quick item entry. NSPhotoLibraryUsageDescription Sage needs access to your photos to attach images to items. UIBackgroundModes fetch remote-notification ``` #### 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((ref) { return InventoryRepositoryImpl(); }); // Provider for controller final inventoryControllerProvider = StateNotifierProvider>>((ref) { final repository = ref.watch(inventoryRepositoryProvider); return InventoryController(repository); }); // Controller class class InventoryController extends StateNotifier>> { final InventoryRepository _repository; InventoryController(this._repository) : super(const AsyncValue.loading()) { loadItems(); } Future 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 addItem(FoodItem item) async { await _repository.addItem(item); await loadItems(); // Refresh list } Future 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> getAllItems(); Future getItemById(int id); Future addItem(FoodItem item); Future updateItem(FoodItem item); Future deleteItem(int id); Future> getItemsExpiringWithinDays(int days); Future> 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> getAllItems() async { return await _isar.foodItems.where().findAll(); } @override Future getItemById(int id) async { return await _isar.foodItems.get(id); } @override Future addItem(FoodItem item) async { await _isar.writeTxn(() async { await _isar.foodItems.put(item); }); } @override Future updateItem(FoodItem item) async { await _isar.writeTxn(() async { await _isar.foodItems.put(item); }); } @override Future deleteItem(int id) async { await _isar.writeTxn(() async { await _isar.foodItems.delete(id); }); } @override Future> getItemsExpiringWithinDays(int days) async { final targetDate = DateTime.now().add(Duration(days: days)); return await _isar.foodItems .filter() .expirationDateLessThan(targetDate) .findAll(); } @override Future> 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((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 runDailyCheck() async { final userSettings = await settings.getSettings(); final allItems = await inventory.getAllItems(); for (final item in allItems) { await _checkAndNotify(item, userSettings); } } Future _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! 🌿πŸ”₯**