Daehan Lim

Header Project Icon

Cooki - AI Recipe Community App

πŸ“ Overview

πŸ“Œ App Introduction: Personalized recipe generation and sharing platform powered by generative AI
πŸ•’ Duration: June 1, 2025 ~ July 4, 2025 (1 month)
πŸ“± Platform: Flutter cross-platform app (iOS, Android)
πŸ‘₯ Team Size: 3 developers
πŸ’Ό Role: AI recipe generation, recipe editing, review features, reporting system, internationalization, etc.
πŸ› οΈ Key Technologies: Flutter Dart Firebase Riverpod MVVM Gemini API Firestore Dio Cloud Functions Google Cloud Translation API
πŸ”— GitHub: flutter-fantastic-four/cooki-app
πŸ”— App Store: apps.apple.com/kr/app/cooki/id6747327839

1 ad screen 2 ad screen Recipe generation screen Recipe detail screen Community screen Review screen My recipes screen

πŸ“– Project Background

πŸ› οΈ Tech Stack

Flutter Dart Riverpod MVVM Firebase Firestore Dio Cloud Functions Gemini AI Crashlytics Firebase Storage Google Translate API Cached Network Image Image Picker Image Compress Share Plus SharedPreferences i18n Speech Recognition Easy Image Viewer

🌟 Key Contributions

AI Recipe Generation and Management System

Review System and Translation Features

Internationalization and Speech Recognition

Recipe External Sharing Feature

UI/UX Optimization and Performance Improvements

Architecture and Exception Handling System

🧭 Technical Decision-Making

1. Gemini AI Model Selection and 2-Stage Validation System

// Validation Model Configuration
_validationModel = googleAI.generativeModel(
  model: 'gemini-1.5-flash',
  generationConfig: GenerationConfig(
    responseMimeType: 'application/json',
    responseSchema: Schema.object(
      properties: {'isValid': Schema.boolean()},
    ),
  ),
);

// Generation Model Configuration
_recipeGenerationModel = googleAI.generativeModel(
  model: 'gemini-2.0-flash',
  generationConfig: GenerationConfig(
    responseMimeType: 'application/json',
    responseSchema: Schema.object(/* Recipe structure definition */),
  ),
);

2. Firebase Cloud Functions-based Translation System

exports.translateText = onCall({ region: "asia-northeast3" }, async (request) => {
  try {
    const { text, targetLanguage, sourceLanguage } = request.data;
    
    const translationRequest = {
      parent: `projects/${projectId}/locations/global`,
      contents: [text],
      mimeType: 'text/plain',
      targetLanguageCode: targetLanguage,
      ...(sourceLanguage && { sourceLanguageCode: sourceLanguage }),
    };
    
    const [response] = await translationClient.translateText(translationRequest);
    
    return {
      success: true,
      translatedText: response.translations[0].translatedText,
      detectedSourceLanguage: response.translations[0].detectedLanguageCode || sourceLanguage
    };
  } catch (error) {
    throw new Error('Translation failed: ' + error.message);
  }
});

3. Unified Logging and Crash Monitoring Utility

void logError(
  dynamic error,
  StackTrace stack, {
  String? reason,
  bool fatal = false,
}) {
  final message = reason != null 
      ? '[EXCEPTION] $reason\n$error' 
      : '[EXCEPTION] $error';
  log(message, stackTrace: stack);

  FirebaseCrashlytics.instance.recordError(
    error,
    stack,
    reason: reason,
    fatal: fatal,
  );
}

// Usage Example
try {
  final bytes = await imageDownloadRepository.downloadImage(
    recipe.imageUrl!,
  );
  ...
} catch (e, stack) {
  logError(e, stack, reason: 'Image download failed');
}

4. Multimodal Prompt Engineering

Future<String> _buildRecipePrompt({
  String? textInput,
  Set<String>? preferences,
  required bool hasImage,
  required String textOnlyRecipePromptPath,
  required String imageRecipePromptPath,
}) async {
  if (hasImage) {
    String imagePrompt = await rootBundle.loadString(
      'assets/prompts/$imageRecipePromptPath',
    );
    
    // Dynamic section configuration
    String textContextSection = textInput?.isNotEmpty == true 
        ? await _buildTextContextSection(textInput!)
        : '';
    String preferencesSection = await _buildPreferencesSection(preferences);
    
    return imagePrompt
        .replaceAll(AppConstants.textContextSectionPlaceholder, textContextSection)
        .replaceAll(AppConstants.preferencesSectionPlaceholder, preferencesSection);
  }
  // Text-only prompt handling...
}

🌱 Problem Solving

1. Recipe Generation and Image Upload Parallel Processing Optimization

// Original Sequential Processing
final generatedRecipe = await _generateRecipe(
  imageBytes: compressedImageBytes, // Send Uint8List binary data to AI
  textInput: state.textInput,
  // ...
);
if (generatedRecipe != null) {
  final imageUrl = await _uploadImageToStorage(compressedImageBytes, user.id);
  final saved = await _saveRecipe(generatedRecipe, user, imageUrl);
}

// Improved Parallel Processing
final generationTask = _generateRecipe(...);
final imageUploadTask = _uploadImageIfNeeded(...);
final results = await Future.wait([generationTask, imageUploadTask]);

final generated = results[0] as GeneratedRecipe?;
final imageUrl = results[1] as String?;

2. Review Language Detection Optimization

// Original Synchronous Processing
await saveReview(review);
await detectAndUpdateLanguage(reviewId); // UI blocking

// Improved Asynchronous Processing
final reviewId = await saveReview(review);
detectAndUpdateLanguage(reviewId); // Background execution without await

3. Image Optimization to Reduce API Costs and Improve Performance

4. Filename Collision in Parallel Image Uploads

🎞️ Video

Watch the Video