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

29 KiB

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:

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:

    # 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:

    flutter doctor
    

    Fix any issues reported (Android Studio, Xcode, etc.)

  3. Install Android Studio:


Project Creation

  1. Create Flutter Project:

    flutter create sage
    cd sage
    
  2. Update pubspec.yaml:

    • Replace with the pubspec.yaml content above
    • Run: flutter pub get
  3. Generate Isar Files:

    flutter pub run build_runner build
    
  4. Set Up Git:

    git init
    git add .
    git commit -m "Initial commit"
    

Android Configuration

1. Update AndroidManifest.xml

File: android/app/src/main/AndroidManifest.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

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

<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

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

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

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

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

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

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

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

// 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:

# Format all Dart files
dart format .

# Analyze code for issues
flutter analyze

# Run tests
flutter test

🚀 DEVELOPMENT WORKFLOW

1. Start Development

# Get dependencies
flutter pub get

# Generate code (Isar, Riverpod)
flutter pub run build_runner watch

# Run app
flutter run

2. Development Mode

# Hot reload: Press 'r' in terminal
# Hot restart: Press 'R'
# Open DevTools: Press 'w'

3. Testing

# 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

# 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

Tutorials

Community


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! 🌿🔥