MealVeda Day 4: Production Challenges - Error Handling and Data Reliability

Day 4 focused on the unglamorous but critical work of making MealVeda production-ready. This meant implementing robust error handling, user feedback systems, and data normalization layers that transform a functional demo into a reliable app.
API Reliability Architecture
Open-source nutrition APIs aren't designed for consumer app reliability. Implemented a multi-tiered fallback system:
class NutritionService {
final List<NutritionAPI> _apis = [
EdamamAPI(),
USDAFoodDataAPI(),
NutritionixAPI(),
];
Future<NutritionData> getNutritionData(String ingredient) async {
NutritionData? result;
// Try each API in order
for (final api in _apis) {
try {
result = await api.fetchNutrition(ingredient).timeout(
Duration(seconds: 5),
);
if (result.isComplete) return result;
} catch (e) {
_logAPIFailure(api, e);
continue; // Try next API
}
}
// All APIs failed, try cache
result = await _getCachedNutrition(ingredient);
if (result != null) return result.markAsEstimated();
// Generate reasonable estimate
return _generateEstimate(ingredient);
}
}
Data Normalization Layer
Nutrition APIs return inconsistent data formats. Built a normalization system:
class NutritionNormalizer {
static NutritionData normalize(Map<String, dynamic> apiResponse, APIType type) {
switch (type) {
case APIType.edamam:
return _normalizeEdamam(apiResponse);
case APIType.usda:
return _normalizeUSDA(apiResponse);
case APIType.nutritionix:
return _normalizeNutritionix(apiResponse);
}
}
static NutritionData _normalizeEdamam(Map<String, dynamic> data) {
return NutritionData(
calories: _extractCalories(data, 'ENERC_KCAL'),
protein: _extractMacro(data, 'PROCNT', MacroUnit.grams),
carbs: _extractMacro(data, 'CHOCDF', MacroUnit.grams),
fat: _extractMacro(data, 'FAT', MacroUnit.grams),
fiber: _extractMicro(data, 'FIBTG', MacroUnit.grams),
);
}
static double _extractMacro(Map data, String key, MacroUnit expectedUnit) {
final nutrient = data['totalNutrients'][key];
if (nutrient == null) return 0.0;
double value = nutrient['quantity'].toDouble();
String unit = nutrient['unit'];
// Convert to standard units (grams)
switch (unit) {
case 'mg': return value / 1000;
case 'µg': return value / 1000000;
case 'g': return value;
default: return value;
}
}
}
In-App Feedback System
Implemented context-aware feedback collection:
class FeedbackService {
Future<void> submitFeedback({
required FeedbackType type,
required String message,
String? userEmail,
}) async {
final contextData = await _gatherContext();
final feedback = FeedbackSubmission(
type: type,
message: message,
userEmail: userEmail,
context: contextData,
timestamp: DateTime.now(),
);
await _submitToBackend(feedback);
await _storeLocalBackup(feedback);
}
Future<ContextData> _gatherContext() async {
final deviceInfo = await DeviceInfoPlugin().androidInfo;
return ContextData(
appVersion: await _getAppVersion(),
platform: Platform.operatingSystem,
deviceModel: deviceInfo.model,
androidVersion: deviceInfo.version.release,
currentScreen: _getCurrentScreenName(),
userActions: _getRecentUserActions(),
errorLogs: _getRecentErrors(),
);
}
}
class FeedbackScreen extends StatefulWidget {
@override
_FeedbackScreenState createState() => _FeedbackScreenState();
}
class _FeedbackScreenState extends State<FeedbackScreen> {
final _messageController = TextEditingController();
FeedbackType _selectedType = FeedbackType.feature;
Widget _buildFeatureRequests() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Feature Requests', style: Theme.of(context).textTheme.headline6),
SizedBox(height: 8),
..._featureRequests.map((request) => _buildFeatureRequestTile(request)),
],
);
}
Widget _buildFeatureRequestTile(FeatureRequest request) {
return ListTile(
leading: Icon(
request.status == FeatureStatus.completed
? Icons.check_circle
: Icons.lightbulb_outline,
color: request.status == FeatureStatus.completed
? Colors.green
: Colors.orange,
),
title: Text(request.title),
subtitle: Text(request.description),
trailing: request.status == FeatureStatus.completed
? Text('Completed', style: TextStyle(color: Colors.green))
: null,
);
}
}
Offline Data Persistence
Implemented local caching for API responses and user data:
class CacheManager {
static const String _nutritionCacheKey = 'nutrition_cache';
static const String _mealsCacheKey = 'meals_cache';
Future<void> cacheNutritionData(String ingredient, NutritionData data) async {
final prefs = await SharedPreferences.getInstance();
final cache = _getNutritionCache(prefs);
cache[ingredient] = {
'data': data.toJson(),
'timestamp': DateTime.now().millisecondsSinceEpoch,
'ttl': Duration(days: 7).inMilliseconds,
};
await prefs.setString(_nutritionCacheKey, jsonEncode(cache));
}
Future<NutritionData?> getCachedNutrition(String ingredient) async {
final prefs = await SharedPreferences.getInstance();
final cache = _getNutritionCache(prefs);
final cachedEntry = cache[ingredient];
if (cachedEntry == null) return null;
final timestamp = cachedEntry['timestamp'];
final ttl = cachedEntry['ttl'];
final now = DateTime.now().millisecondsSinceEpoch;
if (now - timestamp > ttl) {
// Cache expired
cache.remove(ingredient);
await prefs.setString(_nutritionCacheKey, jsonEncode(cache));
return null;
}
return NutritionData.fromJson(cachedEntry['data']);
}
}
Error Recovery Mechanisms
Built comprehensive error handling for production scenarios:
class ErrorHandler {
static void handleError(dynamic error, StackTrace stackTrace) {
// Log to analytics
FirebaseCrashlytics.instance.recordError(error, stackTrace);
// Show user-friendly message
if (error is SocketException) {
_showNoInternetDialog();
} else if (error is TimeoutException) {
_showTimeoutDialog();
} else if (error is FormatException) {
_showDataErrorDialog();
} else {
_showGenericErrorDialog();
}
}
static void _showNoInternetDialog() {
Get.snackbar(
'Connection Issue',
'Please check your internet connection. Using cached data where available.',
icon: Icon(Icons.wifi_off),
backgroundColor: Colors.orange.withOpacity(0.8),
);
}
}
Day 4 Technical Achievements:
Multi-tier API fallback system with graceful degradation
Comprehensive data normalization across different API formats
Context-aware user feedback collection and management
Offline-first architecture with intelligent caching
Production-ready error handling and recovery mechanisms
These infrastructure improvements enabled the Android beta launch by ensuring reliability over feature completeness.



