Skip to main content

Command Palette

Search for a command to run...

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

Published
4 min read
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.