Skip to main content

Command Palette

Search for a command to run...

MealVeda Day 3: Breakthrough - Implementing Advanced Bottom Sheet Navigation

Published
3 min read
MealVeda Day 3: Breakthrough - Implementing Advanced Bottom Sheet Navigation

Day 3 delivered MealVeda's most technically complex feature: the bottom sheet meal selection interface. This implementation required careful state management, smooth animations, and real-time data synchronization.

Bottom Sheet Architecture

The meal selection system uses Flutter's showModalBottomSheet with custom enhancements for contextual meal display:

void _showMealSelection(MealCategory category) {
  showModalBottomSheet<Meal>(
    context: context,
    isScrollControlled: true,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
    ),
    builder: (context) => MealSelectionSheet(
      category: category,
      availableMeals: _getMealsForCategory(category),
      onMealSelected: _addMealToDay,
    ),
  );
}

Dynamic Meal Filtering

Implemented contextual meal filtering based on category and time of day:

List<Meal> _getMealsForCategory(MealCategory category) {
  return userMealCollection
    .where((meal) => 
      meal.category == category || 
      meal.isAppropriateForTime(DateTime.now())
    )
    .take(8) // Limit to prevent overwhelming users
    .toList();
}

extension MealTiming on Meal {
  bool isAppropriateForTime(DateTime time) {
    final hour = time.hour;
    switch (category) {
      case MealCategory.breakfast:
        return hour >= 6 && hour < 11;
      case MealCategory.lunch:
        return hour >= 11 && hour < 16;
      case MealCategory.dinner:
        return hour >= 16 && hour < 22;
      case MealCategory.snacks:
        return true; // Snacks appropriate anytime
    }
  }
}

Real-Time State Synchronization

The critical technical challenge was maintaining state synchronization between the bottom sheet and parent screen:

class MealSelectionSheet extends StatefulWidget {
  final MealCategory category;
  final List<Meal> availableMeals;
  final Function(Meal) onMealSelected;

  @override
  _MealSelectionSheetState createState() => _MealSelectionSheetState();
}

class _MealSelectionSheetState extends State<MealSelectionSheet> {
  void _selectMeal(Meal meal) {
    // Update parent state immediately
    widget.onMealSelected(meal);

    // Trigger nutrition recalculation
    Provider.of<NutritionTracker>(context, listen: false)
      .addMealToDay(meal);

    // Close bottom sheet with animation
    Navigator.of(context).pop(meal);
  }
}

Inline Meal Creation

The "Create New Meal" functionality needed to work seamlessly within the bottom sheet context:

Widget _buildCreateNewOption() {
  return ListTile(
    leading: CircleAvatar(
      backgroundColor: Colors.green,
      child: Icon(Icons.add, color: Colors.white),
    ),
    title: Text('Create New ${widget.category.name}'),
    subtitle: Text('Add a new meal to your collection'),
    onTap: () => _showInlineCreation(),
  );
}

void _showInlineCreation() {
  // Expand bottom sheet to full screen
  Navigator.of(context).push(
    PageRouteBuilder(
      pageBuilder: (context, animation, secondaryAnimation) => 
        CreateMealScreen(
          initialCategory: widget.category,
          onMealCreated: (meal) {
            widget.onMealSelected(meal);
            Navigator.of(context).pop(); // Return to day view
          },
        ),
      transitionsBuilder: (context, animation, secondaryAnimation, child) {
        return SlideTransition(
          position: animation.drive(
            Tween(begin: Offset(0.0, 1.0), end: Offset.zero)
          ),
          child: child,
        );
      },
    ),
  );
}

Performance Optimizations

Critical optimizations for smooth 60fps animations:

class OptimizedMealList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      // Use builder for memory efficiency
      itemCount: widget.meals.length,
      cacheExtent: 100, // Reduce render overhead
      physics: BouncingScrollPhysics(),
      itemBuilder: (context, index) {
        return MealListTile(
          meal: widget.meals[index],
          onTap: () => _selectMeal(widget.meals[index]),
        );
      },
    );
  }
}

// Prevent unnecessary rebuilds
class MealListTile extends StatelessWidget {
  final Meal meal;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
        title: Text(meal.name),
        subtitle: Text('${meal.calories} cal • ${meal.protein}g protein'),
        trailing: Icon(Icons.add_circle_outline, color: Colors.green),
        onTap: onTap,
      ),
    );
  }
}

Animation System

Implemented smooth slide animations for state transitions:

class SlideUpRoute<T> extends PageRouteBuilder<T> {
  final Widget child;

  SlideUpRoute({required this.child})
    : super(
        pageBuilder: (context, animation, secondaryAnimation) => child,
        transitionsBuilder: (context, animation, secondaryAnimation, child) {
          const begin = Offset(0.0, 1.0);
          const end = Offset.zero;
          final tween = Tween(begin: begin, end: end);
          final offsetAnimation = animation.drive(tween);

          return SlideTransition(position: offsetAnimation, child: child);
        },
      );
}

Day 3 Technical Achievements:

  • Context-aware meal filtering and display

  • Real-time state synchronization across components

  • Inline meal creation with smooth transitions

  • 60fps bottom sheet animations and interactions

  • Memory-efficient list rendering for large meal collections

This bottom sheet implementation became MealVeda's signature interaction pattern, differentiating it from traditional meal planning interfaces.