Flutter Food Database API Integration Tutorial
Published June 5, 2026
A Flutter food database API integration turns your macro tracker from a static prototype into a product users can actually log meals with. This tutorial walks through search, food detail, barcode lookup, and the backend proxy pattern so your API key never ships inside the APK.
We use Calorie API as the food database because it combines search, suggest, barcode, and per-100g macros in one REST API with a free developer tier.
Prerequisites
| Item | Version |
|---|---|
| Flutter | 3.16+ |
| Dart | 3.2+ |
| http or dio package | Latest stable |
| Backend URL | Next.js, Firebase, or Supabase proxy |
Get a free key at Calorie API registration.
Step 1: Add Dependencies
# pubspec.yaml
dependencies:
http: ^1.2.0
Run flutter pub get.
Step 2: Create Models
class FoodSearchResult {
final int id;
final String name;
final String? brand;
final double? calories;
FoodSearchResult({
required this.id,
required this.name,
this.brand,
this.calories,
});
factory FoodSearchResult.fromJson(Map<String, dynamic> json) {
return FoodSearchResult(
id: json['id'] as int,
name: json['name'] as String,
brand: json['brand'] as String?,
calories: (json['calories'] as num?)?.toDouble(),
);
}
}
Typed models catch null fields before they crash your ListView.
Step 3: API Service (Through Your Backend)
Never hardcode X-API-Key in Flutter source. Call your proxy:
import 'dart:convert';
import 'package:http/http.dart' as http;
class NutritionApiService {
NutritionApiService({required this.baseUrl});
final String baseUrl;
Future<List<FoodSearchResult>> searchFoods(String query, {int limit = 15}) async {
final uri = Uri.parse('$baseUrl/api/foods/search')
.replace(queryParameters: {'q': query, 'limit': '$limit'});
final res = await http.get(uri);
if (res.statusCode != 200) {
throw Exception('Search failed: ${res.statusCode}');
}
final body = jsonDecode(res.body) as Map<String, dynamic>;
final results = body['results'] as List<dynamic>;
return results.map((e) => FoodSearchResult.fromJson(e as Map<String, dynamic>)).toList();
}
Future<Map<String, dynamic>> getFoodById(int id) async {
final uri = Uri.parse('$baseUrl/api/foods/$id');
final res = await http.get(uri);
if (res.statusCode != 200) throw Exception('Detail failed');
return jsonDecode(res.body) as Map<String, dynamic>;
}
Future<Map<String, dynamic>> lookupBarcode(String upc) async {
final uri = Uri.parse('$baseUrl/api/foods/barcode/${Uri.encodeComponent(upc)}');
final res = await http.get(uri);
if (res.statusCode != 200) throw Exception('Barcode not found');
return jsonDecode(res.body) as Map<String, dynamic>;
}
}
See secure Next.js API route or Firebase Cloud Function proxy for backend setup.
Step 4: Debounced Search Widget
import 'dart:async';
import 'package:flutter/material.dart';
class FoodSearchPage extends StatefulWidget {
const FoodSearchPage({super.key, required this.api});
final NutritionApiService api;
@override
State<FoodSearchPage> createState() => _FoodSearchPageState();
}
class _FoodSearchPageState extends State<FoodSearchPage> {
final _controller = TextEditingController();
Timer? _debounce;
List<FoodSearchResult> _results = [];
bool _loading = false;
@override
void initState() {
super.initState();
_controller.addListener(_onQueryChanged);
}
void _onQueryChanged() {
_debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 300), () async {
final q = _controller.text.trim();
if (q.length < 2) {
setState(() => _results = []);
return;
}
setState(() => _loading = true);
try {
final data = await widget.api.searchFoods(q);
setState(() => _results = data);
} finally {
setState(() => _loading = false);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Log food')),
body: Column(
children: [
TextField(controller: _controller, decoration: const InputDecoration(hintText: 'Search foods')),
if (_loading) const LinearProgressIndicator(),
Expanded(
child: ListView.builder(
itemCount: _results.length,
itemBuilder: (context, i) {
final food = _results[i];
return ListTile(
title: Text(food.name),
subtitle: Text('${food.calories?.toStringAsFixed(0) ?? 'N/A'} kcal / 100g'),
onTap: () => Navigator.pushNamed(context, '/food/${food.id}'),
);
},
),
),
],
),
);
}
}
Step 5: Scale Portions
double scaleNutrient(double per100g, double grams) => (grams / 100) * per100g;
Load full detail from getFoodById, multiply protein, carbs, and fat by user grams.
Step 6: Barcode With mobile_scanner
dependencies:
mobile_scanner: ^5.0.0
On scan success, call lookupBarcode(barcode.rawValue!) and navigate to the log screen.
Production Checklist
- Proxy API key on server (backend proxy pattern)
- Cache recent foods in
shared_preferencesor Hive - Handle offline with cached results
- Monitor Calorie API quota before Play Store launch
Related Guides
Frequently Asked Questions
How do I integrate a food database API in Flutter?
Create typed Dart models, call your backend proxy with the http package, debounce search input by 300 ms, and scale per-100g nutrients by the user's portion in grams.
Which food database API works best with Flutter?
Calorie API is a strong choice for Flutter apps because it provides REST search, suggest, barcode lookup, and per-100g macros with a free tier and predictable JSON.
Should I store my API key in Flutter?
No. APK files can be decompiled. Proxy requests through Firebase, Next.js, or Supabase and keep the Calorie API key server-side only.
How do I add barcode scanning in Flutter?
Use mobile_scanner or similar, read the UPC, and call your backend barcode endpoint which forwards to Calorie API search/barcode.
Can Flutter apps use Calorie API for commercial apps?
Yes. Start on the free tier for development and upgrade to a paid plan when you monetize or exceed monthly request limits.
