> ## Documentation Index
> Fetch the complete documentation index at: https://docs.spikeapi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Flutter SDK Nutrition AI

> Analyze food images and retrieve nutritional information using AI-powered analysis in your Flutter app.

## About

The Spike SDK provides a convenient interface for the [Nutrition AI API](/nutrition-ai/overview), allowing you to analyze food images directly from your Flutter application. The SDK handles image encoding, API communication, and response parsing, making it easy to integrate nutritional analysis into your app.

<Note>
  All Spike SDK async methods return `Future` objects. Use `try-catch` blocks or `.catchError()` handlers for error handling. See [Error Handling](#error-handling) for details.
</Note>

## Key Features

* **AI-Powered Analysis** — advanced computer vision for food identification and nutritional calculations
* **Flexible Processing** — choose between synchronous (wait for results) or asynchronous (background) processing
* **Base64 Support** — submit images as base64-encoded strings
* **Complete Record Management** — retrieve, update, and delete nutrition records

## Available Methods

| Method                                                           | Description                                                                                        |
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `analyzeNutrition(imageBase64:, consumedAt:, config:)`           | Submit food image for synchronous processing and wait for the analysis results                     |
| `submitNutritionForAnalysis(imageBase64:, consumedAt:, config:)` | Submit food image for asynchronous processing and get record ID immediately for polling afterwards |
| `getNutritionRecords(from:, to:)`                                | Retrieve nutrition records for a datetime range                                                    |
| `getNutritionRecord(id:)`                                        | Get a specific nutrition record by ID                                                              |
| `updateNutritionRecordServingSize(id:, servingSize:)`            | Update serving size for a nutrition record                                                         |
| `deleteNutritionRecord(id:)`                                     | Delete a nutrition record by ID                                                                    |

## Analyzing Food Images

### Synchronous Processing

Use synchronous analysis when you want to wait for the complete nutritional analysis before proceeding. This is ideal for scenarios where you need immediate results and can display a loading indicator.

```dart theme={null}
import 'dart:convert';
import 'dart:io';
import 'package:spike_flutter_sdk/spike_flutter_sdk.dart';

// Capture image from camera or gallery and convert to base64
final File imageFile = // ... captured from camera or gallery
final bytes = await imageFile.readAsBytes();
final imageBase64 = base64Encode(bytes);

try {
  final record = await spikeConnection.analyzeNutrition(
    imageBase64: imageBase64,
    consumedAt: DateTime.now(),
    config: NutritionalAnalysisConfig(
      analysisMode: NutritionRecordAnalysisMode.precise,
      countryCode: 'us',
      languageCode: 'en',
      includeNutriScore: true,
      includeDishDescription: true,
      includeIngredients: true,
      includeNutritionFields: [
        NutritionalField.energyKcal,
        NutritionalField.proteinG,
        NutritionalField.fatTotalG,
        NutritionalField.carbohydrateG,
      ],
    ),
  );

  print('Dish: ${record.dishName ?? "Unknown"}');
  print('Serving size: ${record.servingSize ?? 0} ${record.unit?.toJson() ?? "g"}');
  print('Calories: ${record.nutritionalFields?["energy_kcal"] ?? 0}');
} catch (e) {
  print('Analysis failed: $e');
}
```

You can also call with minimal parameters (config is optional):

```dart theme={null}
// Using defaults - only imageBase64 is required
final record = await spikeConnection.analyzeNutrition(
  imageBase64: imageBase64,
);
```

<Note>
  **Processing Time**: Synchronous processing takes some time depending on image complexity. Consider showing a loading indicator to users. If you see that the analysis is taking too long, the recommendation is to use asynchronous processing instead.
</Note>

### Asynchronous Processing

Use asynchronous processing when you want an immediate response without waiting for the analysis to complete. Record ID is returned.
The image is processed in the background, and you can retrieve results later by requesting nutrition analysis using the record ID or receive them via webhook.

```dart theme={null}
try {
  // Submit image for background processing
  final recordId = await spikeConnection.submitNutritionForAnalysis(
    imageBase64: imageBase64,
    consumedAt: DateTime.now(),
    config: NutritionalAnalysisConfig(
      analysisMode: NutritionRecordAnalysisMode.fast,
      countryCode: null,
      languageCode: null,
      includeNutriScore: null,
      includeDishDescription: null,
      includeIngredients: true,
      includeNutritionFields: null,
    ),
  );

  print('Analysis started. Record ID: $recordId');

  // Optionally, poll for results later
  // Your backend will also receive a webhook when analysis completes

} catch (e) {
  print('Failed to submit: $e');
}
```

#### Retrieving Results Asynchronously

After submitting an image for asynchronous processing, you can retrieve the results using the record ID. Check the processing status for completion success.

```dart theme={null}
// Check the status and get results
final record = await spikeConnection.getNutritionRecord(id: recordId);

if (record != null) {
  switch (record.status) {
    case NutritionRecordStatus.completed:
      print('Analysis complete: ${record.dishName ?? "Unknown"}');
      break;
    case NutritionRecordStatus.processing:
      print('Still processing...');
      break;
    case NutritionRecordStatus.pending:
      print('Queued for processing...');
      break;
    case NutritionRecordStatus.failed:
      print('Analysis failed: ${record.failureReason ?? "Unknown error"}');
      break;
    case NutritionRecordStatus.unknown:
      print('Unknown status');
      break;
  }
}
```

<Tip>
  For real-time notifications, configure webhooks in your [admin console](https://admin.spikeapi.com/). Your backend will receive a webhook notification when the analysis completes. See [Asynchronous Processing](/nutrition-ai/async) for webhook implementation details.
</Tip>

## Configuration Options

Customize the analysis using `NutritionalAnalysisConfig`:

```dart theme={null}
final config = NutritionalAnalysisConfig(
  // Analysis speed vs. precision
  analysisMode: NutritionRecordAnalysisMode.precise,  // precise (default) or fast
  
  // Country ISO 3166-1 alpha-2 code in lowercase
  countryCode: 'us',

  // Language ISO 639-1 code in lowercase
  languageCode: 'en',
  
  // Include Nutri-Score rating (A-E)
  includeNutriScore: true,
  
  // Include dish description
  includeDishDescription: true,
  
  // Include detailed breakdown of ingredients
  includeIngredients: true,
  
  // Specify which nutritional fields to include
  includeNutritionFields: [
    NutritionalField.energyKcal,
    NutritionalField.proteinG,
    NutritionalField.fatTotalG,
    NutritionalField.carbohydrateG,
    NutritionalField.fiberTotalDietaryG,
    NutritionalField.sodiumMg,
  ],
);

final record = await spikeConnection.analyzeNutrition(
  imageBase64: imageBase64,
  consumedAt: DateTime.now(),
  config: config,
);
```

### `NutritionalAnalysisConfig`

```dart theme={null}
class NutritionalAnalysisConfig {
  /// A preferred mode for the analysis. Default is precise.
  final NutritionRecordAnalysisMode? analysisMode;
  /// Country ISO 3166-1 alpha-2 code in lowercase
  final String? countryCode;
  /// Language ISO 639-1 code in lowercase
  final String? languageCode;
  /// Include nutri-score label of the food. Default is false.
  final bool? includeNutriScore;
  /// Include dish description of the food. Default is false.
  final bool? includeDishDescription;
  /// Include ingredients of the food. Default is false.
  final bool? includeIngredients;
  /// Include specific nutrition fields in the analysis report.
  /// By default, carbohydrate_g, energy_kcal, fat_total_g and protein_g will be included.
  final List<NutritionalField>? includeNutritionFields;
}
```

### Analysis Modes

```dart theme={null}
enum NutritionRecordAnalysisMode {
  fast,
  precise
}
```

| Mode      | Description                                                                  |
| --------- | ---------------------------------------------------------------------------- |
| `precise` | Uses advanced AI models for highest accuracy and detailed analysis (default) |
| `fast`    | Uses optimized models for quicker processing with good accuracy              |

### Default Nutritional Fields

If `includeNutritionFields` is not specified, only these basic fields are included:

* `energyKcal`
* `proteinG`
* `fatTotalG`
* `carbohydrateG`

See [Nutritional Fields Reference](/technical-references/nutritional_fields) for all available fields.

## Managing Nutrition Records

### List Records by Date Range

Retrieve all nutrition records within a specified date range:

```dart theme={null}
final now = DateTime.now();
final startDate = now.subtract(const Duration(days: 7));
final endDate = now;

try {
  final records = await spikeConnection.getNutritionRecords(
    from: startDate,
    to: endDate,
  );

  for (final record in records) {
    final consumedAt = record.consumedAt?.toString() ?? 'Unknown date';
    final size = record.servingSize ?? 0;
    final unit = record.unit?.toJson() ?? 'g';
    print('$consumedAt: ${record.dishName ?? "Unknown"} - $size$unit');
  }
} catch (e) {
  print('Failed to fetch records: $e');
}
```

### Get a Specific Record

Retrieve a single nutrition record by its ID:

```dart theme={null}
try {
  final record = await spikeConnection.getNutritionRecord(id: recordId);

  if (record != null) {
    print('Dish: ${record.dishName ?? "Unknown"}');
    print('Nutri-Score: ${record.nutriScore ?? "N/A"}');

    // Access nutritional values
    final calories = record.nutritionalFields?['energy_kcal'];
    if (calories != null) {
      print('Calories: $calories kcal');
    }

    // Access ingredients if included
    for (final ingredient in record.ingredients ?? []) {
      print('- ${ingredient.name}: ${ingredient.servingSize}${ingredient.unit.toJson()}');
    }
  } else {
    print('Record not found');
  }
} catch (e) {
  print('Failed to fetch record: $e');
}
```

### Update Serving Size

Adjust the serving size of an existing record. All nutritional values are automatically recalculated proportionally:

```dart theme={null}
try {
  final updatedRecord = await spikeConnection.updateNutritionRecordServingSize(
    id: recordId,
    servingSize: 200.0,  // New serving size in grams
  );

  print('Updated serving size: ${updatedRecord.servingSize ?? 0}${updatedRecord.unit?.toJson() ?? "g"}');
  print('Recalculated calories: ${updatedRecord.nutritionalFields?["energy_kcal"] ?? 0}');
} catch (e) {
  print('Failed to update record: $e');
}
```

### Delete a Record

Permanently remove a nutrition record (success status is returned regardless record is found or not):

```dart theme={null}
try {
  await spikeConnection.deleteNutritionRecord(id: recordId);
  print('Record deleted successfully');
} catch (e) {
  print('Failed to delete record: $e');
}
```

## Response Data

### `NutritionRecord`

The `NutritionRecord` class contains the analysis results:

```dart theme={null}
class NutritionRecord {
  /// Report record ID
  final String recordId;
  /// Processing status
  final NutritionRecordStatus status;
  /// Detected dish name
  final String? dishName;
  /// Detected dish description
  final String? dishDescription;
  /// Dish name translated to target language
  final String? dishNameTranslated;
  /// Dish description translated to target language
  final String? dishDescriptionTranslated;
  /// Nutri-Score known as the 5-Colour Nutrition label (A-E)
  final String? nutriScore;
  /// Reason for processing failure
  final String? failureReason;
  /// Serving size in metric units
  final num? servingSize;
  /// Metric unit (g for solids, ml for liquids)
  final NutritionalUnit? unit;
  /// Nutritional values as key-value pairs
  final Map<String, num>? nutritionalFields;
  /// List of detected ingredients with nutritional information
  final List<NutritionRecordIngredient>? ingredients;
  /// Upload timestamp in UTC
  final DateTime uploadedAt;
  /// Update timestamp in UTC
  final DateTime modifiedAt;
  /// The UTC time when food was consumed
  final DateTime? consumedAt;
}
```

### `NutritionRecordStatus`

```dart theme={null}
enum NutritionRecordStatus {
  pending,
  processing,
  completed,
  failed,
  /// Unknown value was sent from API. SDK should be updated.
  unknown
}
```

### `NutritionalUnit`

```dart theme={null}
enum NutritionalUnit {
  g,      // grams
  mg,     // milligrams
  mcg,    // micrograms
  ml,     // milliliters
  kcal,   // kilocalories
  unknown
}
```

### `NutritionRecordIngredient`

```dart theme={null}
class NutritionRecordIngredient {
  /// Ingredient name using LANGUAL standard terminology
  final String name;
  /// Ingredient name translated to target language
  final String? nameTranslated;
  /// Serving size in metric units
  final num servingSize;
  /// Metric unit (g for solids, ml for liquids)
  final NutritionalUnit unit;
  /// Nutritional values as key-value pairs
  final Map<String, num>? nutritionalFields;
}
```

### `NutritionalField`

Use this enum to specify which nutritional fields to include in the analysis:

```dart theme={null}
enum NutritionalField {
  energyKcal,           // energy_kcal
  carbohydrateG,        // carbohydrate_g
  proteinG,             // protein_g
  fatTotalG,            // fat_total_g
  fatSaturatedG,        // fat_saturated_g
  fatPolyunsaturatedG,  // fat_polyunsaturated_g
  fatMonounsaturatedG,  // fat_monounsaturated_g
  fatTransG,            // fat_trans_g
  fiberTotalDietaryG,   // fiber_total_dietary_g
  sugarsTotalG,         // sugars_total_g
  cholesterolMg,        // cholesterol_mg
  sodiumMg,             // sodium_mg
  potassiumMg,          // potassium_mg
  calciumMg,            // calcium_mg
  ironMg,               // iron_mg
  magnesiumMg,          // magnesium_mg
  phosphorusMg,         // phosphorus_mg
  zincMg,               // zinc_mg
  vitaminARaeMcg,       // vitamin_a_rae_mcg
  vitaminCMg,           // vitamin_c_mg
  vitaminDMcg,          // vitamin_d_mcg
  vitaminEMg,           // vitamin_e_mg
  vitaminKMcg,          // vitamin_k_mcg
  thiaminMg,            // thiamin_mg
  riboflavinMg,         // riboflavin_mg
  niacinMg,             // niacin_mg
  vitaminB6Mg,          // vitamin_b6_mg
  folateMcg,            // folate_mcg
  vitaminB12Mcg         // vitamin_b12_mcg
}
```

## Error Handling

All nutrition methods return `Future` objects that can throw exceptions. Always wrap calls in try-catch blocks:

```dart theme={null}
try {
  final record = await spikeConnection.analyzeNutrition(
    imageBase64: imageBase64,
    consumedAt: DateTime.now(),
  );
  // Handle success
} on SpikeException catch (e) {
  print('Spike error: ${e.message}');
} catch (e) {
  print('Unexpected error: $e');
}
```

### Common Error Scenarios

| Error                | Cause                                   |
| -------------------- | --------------------------------------- |
| Invalid image format | Image is not JPEG, PNG, or WebP         |
| Image too large      | Base64-encoded image exceeds 10MB       |
| Image too small      | Image is smaller than 512×512 pixels    |
| Unauthorized         | Invalid or expired authentication token |
| Analysis timeout     | AI processing took too long             |
| Unidentifiable       | Non-food image                          |

## Image Guidelines

For optimal analysis results, guide your users to capture images that:

1. **Center the food** — capture the plate contents as the main subject
2. **Fill the frame** — ensure the meal occupies most of the image
3. **Use proper lighting** — natural or bright lighting works best
4. **Avoid obstructions** — remove packaging and minimize utensils in frame
5. **Skip filters** — avoid filters that alter the food's appearance

See [Image Guidelines](/nutrition-ai/overview#image-guidelines) for complete recommendations.

## Best Practices

### 1. Request Only What You Need

Each additional field, ingredient breakdown, or optional data increases processing time. Only request what your app actually uses:

```dart theme={null}
// ❌ Don't request everything "just in case"
final config = NutritionalAnalysisConfig(
  analysisMode: null,
  countryCode: null,
  languageCode: null,
  includeNutriScore: true,
  includeDishDescription: true,
  includeIngredients: true,
  includeNutritionFields: NutritionalField.values,  // All 29 fields
);

// ✅ Request only what you need
final config = NutritionalAnalysisConfig(
  includeNutritionFields: [
    NutritionalField.energyKcal,
    NutritionalField.proteinG,
    NutritionalField.carbohydrateG,
    NutritionalField.fatTotalG,
  ],
);
```

### 2. Consider your actual UI requirements:

* Do you display ingredients? If not, skip `includeIngredients`.
* Do you show Nutri-Score? If not, skip `includeNutriScore`.
* Which nutritional values do you actually display? Request only those.

### 3. Choose the Right Processing Mode

* **Synchronous** (`analyzeNutrition`): Use when you need immediate results and can show a loading state
* **Asynchronous** (`submitNutritionForAnalysis`): Use for better UX when you don't need immediate results, or when processing multiple images

### 4. Handle All Status Values

When using asynchronous processing, always check the record status before accessing results:

```dart theme={null}
final record = await spikeConnection.getNutritionRecord(id: recordId);

if (record == null) {
  // Handle not found
  return;
}

if (record.status != NutritionRecordStatus.completed) {
  if (record.status == NutritionRecordStatus.failed) {
    // Handle failure
    print('Failed: ${record.failureReason}');
  } else {
    // Still processing
    print('Status: ${record.status}');
  }
  return;
}

// Safe to access results
print('Dish: ${record.dishName}');
```

### 5. Implement Webhook Handling

For production apps using asynchronous processing, implement [webhook handling](/nutrition-ai/async) on your backend to receive real-time notifications when analysis completes.

### 6. Create Reusable Configuration

If you're using the same settings across your app, create a shared configuration helper:

```dart theme={null}
// nutrition_config.dart
import 'package:spike_flutter_sdk/spike_flutter_sdk.dart';

class NutritionConfig {
  static final standard = NutritionalAnalysisConfig(
    analysisMode: NutritionRecordAnalysisMode.precise,
    countryCode: null,
    languageCode: null,
    includeNutriScore: true,
    includeDishDescription: null,
    includeIngredients: true,
    includeNutritionFields: [
      NutritionalField.energyKcal,
      NutritionalField.proteinG,
      NutritionalField.fatTotalG,
      NutritionalField.carbohydrateG,
      NutritionalField.fiberTotalDietaryG,
    ],
  );
}

// Usage
final record = await spikeConnection.analyzeNutrition(
  imageBase64: imageBase64,
  consumedAt: DateTime.now(),
  config: NutritionConfig.standard,
);
```

## Related Documentation

* [Nutrition AI Overview](/nutrition-ai/overview) — API overview and key features
* [Implementation Guide](/nutrition-ai/implementation) — Detailed API implementation patterns
* [Asynchronous Processing](/nutrition-ai/async) — Webhook configuration and handling
* [Nutritional Fields Reference](/technical-references/nutritional_fields) — Complete list of available nutritional fields
