BMI calculator

📊 BMI Calculator dengan Flutter & Dart

Body Mass Index (BMI) adalah metode sederhana untuk mengetahui apakah berat badan seseorang tergolong sehat berdasarkan tinggi dan berat badan. Pada artikel ini, kita tidak hanya membahas teori BMI, tetapi juga menerapkannya langsung ke dalam aplikasi Flutter berbasis Dart.

Apa itu BMI?

BMI (Body Mass Index) adalah angka yang dihitung dari:

BMI = berat badan (kg) / (tinggi badan (m) × tinggi badan (m))

BMI digunakan sebagai indikator awal untuk melihat apakah seseorang termasuk: underweight, normal, overweight, atau obesitas.

Kenapa Menggunakan Flutter?

🚀 Cross-platform

Satu kode bisa berjalan di Android, Web, dan Desktop.

🎨 UI Interaktif

Animasi, slider, dan respons real-time.

⚡ Realtime Calculation

Perubahan input langsung memperbarui hasil BMI.

🔗 Demo Aplikasi

Aplikasi BMI Calculator ini sudah di-build ke Flutter Web dan bisa dicoba langsung:

▶️ Buka Demo BMI Calculator

📄 Kode Lengkap Flutter (Dart)

Berikut adalah kode lengkap aplikasi BMI Calculator Pro yang ditulis menggunakan Dart & Flutter.



import 'package:flutter/material.dart';
import 'dart:math';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'BMI Calculator Pro',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      home: const BMICalculator(),
    );
  }
}

class BMICalculator extends StatefulWidget {
  const BMICalculator({super.key});

  @override
  State createState() => _BMICalculatorState();
}

class _BMICalculatorState extends State with SingleTickerProviderStateMixin {
  double _height = 170;
  double _weight = 70;
  double _bmi = 0;
  String _bmiCategory = '';
  String _bmiDescription = '';
  Color _bmiColor = Colors.blue;
  
  // Text controllers untuk input teks
  TextEditingController heightController = TextEditingController();
  TextEditingController weightController = TextEditingController();
  
  // Fokus untuk input field
  FocusNode heightFocusNode = FocusNode();
  FocusNode weightFocusNode = FocusNode();
  
  // Tema dan preferensi
  bool _isDarkMode = false;
  
  late AnimationController _animationController;
  late Animation _bmiAnimation;
  
  // Data untuk kategori BMI dengan saran yang lebih personal
  final List> _bmiCategories = [
    {
      'category': 'Underweight', 
      'range': 'Below 18.5', 
      'color': Colors.orange, 
      'icon': Icons.arrow_downward, 
      'description': 'You may need to gain some weight for better health.',
      'tips': [
        'Consume nutrient-dense foods like nuts, avocados, and whole grains',
        'Consider eating 5-6 smaller meals throughout the day',
        'Incorporate healthy fats into your diet',
        'Consult with a nutritionist for personalized advice',
        'Focus on strength training to build muscle mass'
      ],
      'encouragement': 'You\'re on a journey to a healthier you! Small, consistent changes can make a big difference.',
      'warning': 'Being underweight can affect your immune system and energy levels.'
    },
    {
      'category': 'Normal', 
      'range': '18.5 - 24.9', 
      'color': Colors.green, 
      'icon': Icons.check_circle, 
      'description': 'Congratulations! You have a healthy body weight.',
      'tips': [
        'Maintain your current healthy eating habits',
        'Continue regular physical activity',
        'Stay hydrated and get adequate sleep',
        'Monitor your weight monthly to maintain this range',
        'Focus on overall wellness, not just weight'
      ],
      'encouragement': 'Excellent work! You\'re maintaining a healthy lifestyle. Keep up the great habits!',
      'warning': 'Even with a normal BMI, continue regular health check-ups.'
    },
    {
      'category': 'Overweight', 
      'range': '25 - 29.9', 
      'color': Colors.amber, 
      'icon': Icons.warning, 
      'description': 'Consider a healthier diet and regular exercise.',
      'tips': [
        'Start with 30 minutes of moderate exercise daily',
        'Reduce processed foods and sugary drinks',
        'Increase vegetable and fruit intake',
        'Practice mindful eating and portion control',
        'Set realistic, gradual weight loss goals'
      ],
      'encouragement': 'Every healthy choice counts! You have the power to make positive changes for your wellbeing.',
      'warning': 'Being overweight increases risk of certain health conditions.'
    },
    {
      'category': 'Obesity', 
      'range': '30 and above', 
      'color': Colors.red, 
      'icon': Icons.error, 
      'description': 'Consult with a healthcare professional for guidance.',
      'tips': [
        'Seek professional medical advice first',
        'Start with gentle activities like walking or swimming',
        'Focus on sustainable lifestyle changes, not quick fixes',
        'Consider working with a dietitian or health coach',
        'Address emotional eating patterns if present'
      ],
      'encouragement': 'Taking the first step to seek information shows you care about your health. You can do this!',
      'warning': 'Obesity significantly increases health risks; professional guidance is recommended.'
    },
  ];
  
  // Tips kesehatan umum
  final List> _generalTips = [
    {
      'title': 'Mindful Eating',
      'description': 'Eat slowly, savor each bite, and listen to your body\'s hunger signals.',
      'icon': Icons.psychology,
      'color': Colors.purple
    },
    {
      'title': 'Consistency Over Perfection',
      'description': 'Small daily healthy choices matter more than occasional intense efforts.',
      'icon': Icons.trending_up,
      'color': Colors.blue
    },
    {
      'title': 'Sleep Quality',
      'description': 'Prioritize 7-9 hours of quality sleep for metabolism and recovery.',
      'icon': Icons.bedtime,
      'color': Colors.indigo
    },
    {
      'title': 'Stress Management',
      'description': 'Chronic stress affects weight; practice relaxation techniques regularly.',
      'icon': Icons.spa,
      'color': Colors.teal
    },
    {
      'title': 'Hydration',
      'description': 'Drink water throughout the day, especially before meals.',
      'icon': Icons.water_drop,
      'color': Colors.cyan
    },
    {
      'title': 'Social Support',
      'description': 'Share your health goals with supportive friends or family.',
      'icon': Icons.people,
      'color': Colors.green
    },
  ];

  @override
  void initState() {
    super.initState();
    
    // Inisialisasi controller dengan nilai awal
    heightController.text = _height.toStringAsFixed(1);
    weightController.text = _weight.toStringAsFixed(1);
    
    // Setup animation controller
    _animationController = AnimationController(
      duration: const Duration(milliseconds: 800),
      vsync: this,
    );
    
    _bmiAnimation = Tween(begin: 0, end: 0).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Curves.easeOut,
      ),
    )..addListener(() {
      setState(() {});
    });
    
    // Setup listener untuk input teks
    heightController.addListener(() {
      if (heightController.text.isNotEmpty) {
        double? newHeight = double.tryParse(heightController.text);
        if (newHeight != null && newHeight >= 100 && newHeight <= 220) {
          setState(() {
            _height = newHeight;
            _calculateBMI();
          });
        }
      }
    });
    
    weightController.addListener(() {
      if (weightController.text.isNotEmpty) {
        double? newWeight = double.tryParse(weightController.text);
        if (newWeight != null && newWeight >= 30 && newWeight <= 150) {
          setState(() {
            _weight = newWeight;
            _calculateBMI();
          });
        }
      }
    });
    
    _calculateBMI();
  }

  @override
  void dispose() {
    _animationController.dispose();
    heightController.dispose();
    weightController.dispose();
    heightFocusNode.dispose();
    weightFocusNode.dispose();
    super.dispose();
  }

  void _calculateBMI() {
    // Calculate BMI
    double heightInMeters = _height / 100;
    double bmi = _weight / (heightInMeters * heightInMeters);
    
    // Set BMI category and description
    int categoryIndex = 0;
if (bmi < 18.5) {
  categoryIndex = 0; // Underweight
} else if (bmi < 25) {
  categoryIndex = 1; // Normal
} else if (bmi < 30) {
  categoryIndex = 2; // Overweight
} else {
  categoryIndex = 3; // Obesity
}

    
    _bmiCategory = _bmiCategories[categoryIndex]['category'];
    _bmiDescription = _bmiCategories[categoryIndex]['description'];
    _bmiColor = _bmiCategories[categoryIndex]['color'];
    
    // Animate BMI value
    _bmiAnimation = Tween(begin: _bmi, end: bmi).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Curves.easeOut,
      ),
    );
    
    _animationController.forward(from: 0);
    _bmi = bmi;
  }

  void _resetValues() {
    setState(() {
      _height = 170;
      _weight = 70;
      heightController.text = _height.toStringAsFixed(1);
      weightController.text = _weight.toStringAsFixed(1);
      _calculateBMI();
    });
  }

  // Validasi input
  void _validateAndUpdateHeight(String value) {
    if (value.isNotEmpty) {
      double? newHeight = double.tryParse(value);
      if (newHeight != null && newHeight >= 100 && newHeight <= 220) {
        setState(() {
          _height = newHeight;
          _calculateBMI();
        });
      } else {
        // Jika input tidak valid, reset ke nilai sebelumnya
        heightController.text = _height.toStringAsFixed(1);
      }
    } else {
      // Jika kosong, set ke nilai default
      heightController.text = _height.toStringAsFixed(1);
    }
  }
  
  void _validateAndUpdateWeight(String value) {
    if (value.isNotEmpty) {
      double? newWeight = double.tryParse(value);
      if (newWeight != null && newWeight >= 30 && newWeight <= 150) {
        setState(() {
          _weight = newWeight;
          _calculateBMI();
        });
      } else {
        weightController.text = _weight.toStringAsFixed(1);
      }
    } else {
      weightController.text = _weight.toStringAsFixed(1);
    }
  }
  
  // Toggle tema
  void _toggleTheme() {
    setState(() {
      _isDarkMode = !_isDarkMode;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('BMI Calculator Pro'),
        actions: [
          // Tombol toggle tema
          IconButton(
            onPressed: _toggleTheme,
            icon: Icon(
              _isDarkMode ? Icons.light_mode : Icons.dark_mode,
              color: _isDarkMode ? Colors.yellow.shade300 : Colors.grey.shade800,
            ),
            tooltip: _isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode',
          ),
        ],
      ),
      body: Container(
        decoration: BoxDecoration(
          gradient: _isDarkMode
            ? LinearGradient(
                begin: Alignment.topLeft,
                end: Alignment.bottomRight,
                colors: [
                  Colors.grey.shade900,
                  Colors.blueGrey.shade900,
                ],
              )
            : LinearGradient(
                begin: Alignment.topLeft,
                end: Alignment.bottomRight,
                colors: [
                  Colors.blue.shade50,
                  Colors.purple.shade50,
                ],
              ),
        ),
        child: SafeArea(
          child: SingleChildScrollView(
            child: Padding(
              padding: const EdgeInsets.all(20.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // Header dengan animasi
                  _buildHeader(),
                  
                  const SizedBox(height: 20),
                  
                  // Input section dengan card
                  _buildInputSection(),
                  
                  const SizedBox(height: 30),
                  
                  // BMI Result dengan animasi
                  _buildBMIResult(),
                  
                  const SizedBox(height: 30),
                  
                  // Personal Advice berdasarkan kategori BMI
                  _buildPersonalAdvice(),
                  
                  const SizedBox(height: 30),
                  
                  // BMI Categories Info
                  _buildBMICategories(),
                  
                  const SizedBox(height: 30),
                  
                  // Tips Section
                  _buildTipsSection(),
                  
                  const SizedBox(height: 30),
                  
                  // Reset Button
                  _buildResetButton(),
                  
                  const SizedBox(height: 20),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildHeader() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  'BMI Calculator',
                  style: TextStyle(
                    fontSize: 28,
                    fontWeight: FontWeight.bold,
                    color: _isDarkMode ? Colors.white : Colors.blue.shade900,
                  ),
                ),
                Text(
                  'Know Your Body Mass Index',
                  style: TextStyle(
                    fontSize: 14,
                    color: _isDarkMode ? Colors.grey.shade400 : Colors.grey.shade700,
                  ),
                ),
              ],
            ),
            Container(
              padding: const EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: _isDarkMode ? Colors.grey.shade800 : Colors.white,
                borderRadius: BorderRadius.circular(12),
                boxShadow: [
                  BoxShadow(
                    color: _isDarkMode ? Colors.black : Colors.blue.shade100,
                    blurRadius: 8,
                    offset: const Offset(0, 2),
                  ),
                ],
              ),
              // Gunakan Image.network untuk gambar dari URL
              child: Image.network(
                'https://imgs.search.brave.com/-nU1J3XoqBQo2GCh2edxN_qsQSuBpUzZMT9mWJpuKRU/rs:fit:860:0:0:0/g:ce/aHR0cHM6Ly9jZG4u/dmVjdG9yc3RvY2su/Y29tL2kvcHJldmll/dy0xeC81Ny8zNS9i/bWktaW5kZXgtaWNv/bi1oZWFsdGgtbWVh/c3VyZW1lbnQtdmVj/dG9yLTQ0NDA1NzM1/LmpwZw',
                width: 40,
                height: 40,
                fit: BoxFit.contain,
                errorBuilder: (context, error, stackTrace) {
                  // Fallback ke icon jika gambar tidak bisa dimuat
                  return Icon(
                    Icons.monitor_weight,
                    size: 32,
                    color: _isDarkMode ? Colors.blue.shade300 : Colors.blue.shade700,
                  );
                },
              ),
            ),
          ],
        ),
        const SizedBox(height: 10),
        Container(
          height: 4,
          width: 80,
          decoration: BoxDecoration(
            color: _isDarkMode ? Colors.blue.shade300 : Colors.blue.shade400,
            borderRadius: BorderRadius.circular(2),
          ),
        ),
      ],
    );
  }

  Widget _buildInputSection() {
    return Card(
      elevation: 8,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(20),
      ),
      color: _isDarkMode ? Colors.grey.shade800 : Colors.white,
      child: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          children: [
            // Height Input dengan slider dan text field
            _buildHeightInput(),
            
            const SizedBox(height: 30),
            
            // Weight Input dengan slider dan text field
            _buildWeightInput(),
          ],
        ),
      ),
    );
  }

  Widget _buildHeightInput() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(
              'Height',
              style: TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.w600,
                color: _isDarkMode ? Colors.white : Colors.black,
              ),
            ),
            // Container untuk menampilkan nilai tinggi
            Container(
              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
              decoration: BoxDecoration(
                color: _isDarkMode ? Colors.blue.shade900 : Colors.blue.shade50,
                borderRadius: BorderRadius.circular(20),
              ),
              child: Text(
                '${_height.toStringAsFixed(1)} cm',
                style: TextStyle(
                  fontSize: 16,
                  fontWeight: FontWeight.bold,
                  color: _isDarkMode ? Colors.blue.shade200 : Colors.blue.shade800,
                ),
              ),
            ),
          ],
        ),
        const SizedBox(height: 15),
        
        // Text field untuk input manual
        TextField(
          controller: heightController,
          focusNode: heightFocusNode,
          keyboardType: TextInputType.number,
          decoration: InputDecoration(
            labelText: 'Enter height in cm',
            labelStyle: TextStyle(
              color: _isDarkMode ? Colors.grey.shade400 : Colors.grey.shade600,
            ),
            hintText: '170.0',
            border: OutlineInputBorder(
              borderRadius: BorderRadius.circular(12),
            ),
            focusedBorder: OutlineInputBorder(
              borderRadius: BorderRadius.circular(12),
              borderSide: BorderSide(
                color: Colors.blue.shade400,
                width: 2,
              ),
            ),
            prefixIcon: Icon(
              Icons.height,
              color: _isDarkMode ? Colors.blue.shade300 : Colors.blue.shade700,
            ),
            suffixText: 'cm',
          ),
          style: TextStyle(
            fontSize: 16,
            color: _isDarkMode ? Colors.white : Colors.black,
          ),
          onSubmitted: _validateAndUpdateHeight,
          textInputAction: TextInputAction.next,
        ),
        
        const SizedBox(height: 20),
        
        Text(
          'Or use slider:',
          style: TextStyle(
            color: _isDarkMode ? Colors.grey.shade400 : Colors.grey.shade600,
            fontSize: 14,
          ),
        ),
        const SizedBox(height: 10),
        
        SliderTheme(
          data: SliderThemeData(
            trackHeight: 10,
            thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 14),
            overlayShape: const RoundSliderOverlayShape(overlayRadius: 24),
            activeTrackColor: _isDarkMode ? Colors.blue.shade400 : Colors.blue.shade400,
            inactiveTrackColor: _isDarkMode ? Colors.grey.shade700 : Colors.grey.shade300,
            thumbColor: _isDarkMode ? Colors.blue.shade300 : Colors.blue.shade700,
            overlayColor: _isDarkMode ? Colors.blue.shade100.withOpacity(0.2) : Colors.blue.shade100,
          ),
          child: Slider(
            value: _height,
            min: 100,
            max: 220,
            divisions: 120,
            label: _height.toStringAsFixed(1),
            onChanged: (value) {
              setState(() {
                _height = value;
                heightController.text = value.toStringAsFixed(1);
                _calculateBMI();
              });
            },
          ),
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(
              '100 cm',
              style: TextStyle(
                color: _isDarkMode ? Colors.grey.shade500 : Colors.grey.shade600,
              ),
            ),
            Text(
              '220 cm',
              style: TextStyle(
                color: _isDarkMode ? Colors.grey.shade500 : Colors.grey.shade600,
              ),
            ),
          ],
        ),
      ],
    );
  }

  Widget _buildWeightInput() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(
              'Weight',
              style: TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.w600,
                color: _isDarkMode ? Colors.white : Colors.black,
              ),
            ),
            Container(
              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
              decoration: BoxDecoration(
                color: _isDarkMode ? Colors.purple.shade900 : Colors.purple.shade50,
                borderRadius: BorderRadius.circular(20),
              ),
              child: Text(
                '${_weight.toStringAsFixed(1)} kg',
                style: TextStyle(
                  fontSize: 16,
                  fontWeight: FontWeight.bold,
                  color: _isDarkMode ? Colors.purple.shade200 : Colors.purple.shade800,
                ),
              ),
            ),
          ],
        ),
        const SizedBox(height: 15),
        
        // Text field untuk input manual
        TextField(
          controller: weightController,
          focusNode: weightFocusNode,
          keyboardType: TextInputType.number,
          decoration: InputDecoration(
            labelText: 'Enter weight in kg',
            labelStyle: TextStyle(
              color: _isDarkMode ? Colors.grey.shade400 : Colors.grey.shade600,
            ),
            hintText: '70.0',
            border: OutlineInputBorder(
              borderRadius: BorderRadius.circular(12),
            ),
            focusedBorder: OutlineInputBorder(
              borderRadius: BorderRadius.circular(12),
              borderSide: BorderSide(
                color: Colors.purple.shade400,
                width: 2,
              ),
            ),
            prefixIcon: Icon(
              Icons.monitor_weight,
              color: _isDarkMode ? Colors.purple.shade300 : Colors.purple.shade700,
            ),
            suffixText: 'kg',
          ),
          style: TextStyle(
            fontSize: 16,
            color: _isDarkMode ? Colors.white : Colors.black,
          ),
          onSubmitted: _validateAndUpdateWeight,
        ),
        
        const SizedBox(height: 20),
        
        Text(
          'Or use slider:',
          style: TextStyle(
            color: _isDarkMode ? Colors.grey.shade400 : Colors.grey.shade600,
            fontSize: 14,
          ),
        ),
        const SizedBox(height: 10),
        
        SliderTheme(
          data: SliderThemeData(
            trackHeight: 10,
            thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 14),
            overlayShape: const RoundSliderOverlayShape(overlayRadius: 24),
            activeTrackColor: _isDarkMode ? Colors.purple.shade400 : Colors.purple.shade400,
            inactiveTrackColor: _isDarkMode ? Colors.grey.shade700 : Colors.grey.shade300,
            thumbColor: _isDarkMode ? Colors.purple.shade300 : Colors.purple.shade700,
            overlayColor: _isDarkMode ? Colors.purple.shade100.withOpacity(0.2) : Colors.purple.shade100,
          ),
          child: Slider(
            value: _weight,
            min: 30,
            max: 150,
            divisions: 120,
            label: _weight.toStringAsFixed(1),
            onChanged: (value) {
              setState(() {
                _weight = value;
                weightController.text = value.toStringAsFixed(1);
                _calculateBMI();
              });
            },
          ),
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(
              '30 kg',
              style: TextStyle(
                color: _isDarkMode ? Colors.grey.shade500 : Colors.grey.shade600,
              ),
            ),
            Text(
              '150 kg',
              style: TextStyle(
                color: _isDarkMode ? Colors.grey.shade500 : Colors.grey.shade600,
              ),
            ),
          ],
        ),
        const SizedBox(height: 10),
        // Weight increment/decrement buttons
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            _buildWeightButton(Icons.remove, () {
              setState(() {
                if (_weight > 30) {
                  _weight -= 0.5;
                  weightController.text = _weight.toStringAsFixed(1);
                  _calculateBMI();
                }
              });
            }),
            const SizedBox(width: 20),
            _buildWeightButton(Icons.add, () {
              setState(() {
                if (_weight < 150) {
                  _weight += 0.5;
                  weightController.text = _weight.toStringAsFixed(1);
                  _calculateBMI();
                }
              });
            }),
          ],
        ),
      ],
    );
  }

  Widget _buildWeightButton(IconData icon, VoidCallback onPressed) {
    return Container(
      width: 50,
      height: 50,
      decoration: BoxDecoration(
        color: _isDarkMode ? Colors.grey.shade800 : Colors.white,
        shape: BoxShape.circle,
        boxShadow: [
          BoxShadow(
            color: _isDarkMode ? Colors.black : Colors.grey.shade300,
            blurRadius: 6,
            offset: const Offset(0, 3),
          ),
        ],
      ),
      child: IconButton(
        icon: Icon(icon, color: _isDarkMode ? Colors.purple.shade300 : Colors.purple.shade700),
        onPressed: onPressed,
      ),
    );
  }

  Widget _buildBMIResult() {
    int currentCategoryIndex = _bmiCategories.indexWhere((cat) => cat['category'] == _bmiCategory);
    
    return Card(
      elevation: 8,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(20),
      ),
      color: _isDarkMode ? Colors.grey.shade800 : Colors.white,
      child: Container(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topLeft,
            end: Alignment.bottomRight,
            colors: [
              _bmiColor.withOpacity(_isDarkMode ? 0.2 : 0.1),
              _bmiColor.withOpacity(_isDarkMode ? 0.1 : 0.05),
            ],
          ),
          borderRadius: BorderRadius.circular(20),
        ),
        child: Padding(
          padding: const EdgeInsets.all(25.0),
          child: Column(
            children: [
              Text(
                'Your BMI Result',
                style: TextStyle(
                  fontSize: 18,
                  fontWeight: FontWeight.w600,
                  color: _isDarkMode ? Colors.white : Colors.grey.shade800,
                ),
              ),
              const SizedBox(height: 20),
              // Animated BMI Value
              SizedBox(
                height: 120,
                child: Stack(
                  alignment: Alignment.center,
                  children: [
                    // Background circle
                    Container(
                      width: 120,
                      height: 120,
                      decoration: BoxDecoration(
                        shape: BoxShape.circle,
                        color: _bmiColor.withOpacity(_isDarkMode ? 0.15 : 0.1),
                      ),
                    ),
                    // BMI Value
                    Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Text(
                          _bmiAnimation.value.toStringAsFixed(1),
                          style: TextStyle(
                            fontSize: 48,
                            fontWeight: FontWeight.bold,
                            color: _bmiColor,
                          ),
                        ),
                        Text(
                          'kg/m²',
                          style: TextStyle(
                            fontSize: 14,
                            color: _isDarkMode ? Colors.grey.shade400 : Colors.grey.shade600,
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
              const SizedBox(height: 20),
              // BMI Category
              Container(
                padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
                decoration: BoxDecoration(
                  color: _bmiColor.withOpacity(_isDarkMode ? 0.3 : 0.2),
                  borderRadius: BorderRadius.circular(20),
                ),
                child: Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Icon(
                      _bmiCategories[currentCategoryIndex]['icon'],
                      color: _bmiColor,
                    ),
                    const SizedBox(width: 10),
                    Text(
                      _bmiCategory,
                      style: TextStyle(
                        fontSize: 20,
                        fontWeight: FontWeight.bold,
                        color: _bmiColor,
                      ),
                    ),
                  ],
                ),
              ),
              const SizedBox(height: 15),
              // BMI Description
              Text(
                _bmiDescription,
                textAlign: TextAlign.center,
                style: TextStyle(
                  fontSize: 14,
                  color: _isDarkMode ? Colors.grey.shade300 : Colors.grey.shade700,
                  height: 1.5,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
  
  Widget _buildPersonalAdvice() {
    int currentCategoryIndex = _bmiCategories.indexWhere((cat) => cat['category'] == _bmiCategory);
    
    return Card(
      elevation: 8,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(20),
      ),
      color: _isDarkMode ? Colors.grey.shade800 : Colors.white,
      child: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Icon(
                  Icons.psychology,
                  color: _bmiColor,
                ),
                const SizedBox(width: 10),
                Text(
                  'Personalized Advice',
                  style: TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                    color: _isDarkMode ? Colors.white : Colors.grey.shade800,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 15),
            
            // Encouragement Message
            Container(
              padding: const EdgeInsets.all(15),
              decoration: BoxDecoration(
                color: _bmiColor.withOpacity(_isDarkMode ? 0.15 : 0.1),
                borderRadius: BorderRadius.circular(15),
                border: Border.all(
                  color: _bmiColor.withOpacity(0.3),
                  width: 1,
                ),
              ),
              child: Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Icon(
                    Icons.favorite,
                    color: _bmiColor,
                    size: 24,
                  ),
                  const SizedBox(width: 12),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          'Words of Encouragement',
                          style: TextStyle(
                            fontWeight: FontWeight.bold,
                            color: _bmiColor,
                            fontSize: 16,
                          ),
                        ),
                        const SizedBox(height: 5),
                        Text(
                          _bmiCategories[currentCategoryIndex]['encouragement'],
                          style: TextStyle(
                            color: _isDarkMode ? Colors.grey.shade300 : Colors.grey.shade700,
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
            
            const SizedBox(height: 20),
            
            // Important Note
            Container(
              padding: const EdgeInsets.all(15),
              decoration: BoxDecoration(
                color: _isDarkMode ? Colors.red.shade900.withOpacity(0.2) : Colors.red.shade50,
                borderRadius: BorderRadius.circular(15),
                border: Border.all(
                  color: Colors.red.withOpacity(0.3),
                  width: 1,
                ),
              ),
              child: Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Icon(
                    Icons.info,
                    color: Colors.red,
                    size: 24,
                  ),
                  const SizedBox(width: 12),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          'Important Note',
                          style: TextStyle(
                            fontWeight: FontWeight.bold,
                            color: Colors.red,
                            fontSize: 16,
                          ),
                        ),
                        const SizedBox(height: 5),
                        Text(
                          _bmiCategories[currentCategoryIndex]['warning'],
                          style: TextStyle(
                            color: _isDarkMode ? Colors.grey.shade300 : Colors.grey.shade700,
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
            
            const SizedBox(height: 20),
            
            Text(
              'Recommended Actions:',
              style: TextStyle(
                fontSize: 16,
                fontWeight: FontWeight.w600,
                color: _isDarkMode ? Colors.white : Colors.grey.shade800,
              ),
            ),
            const SizedBox(height: 10),
            
            // Actionable Tips
            ...List.generate(
              (_bmiCategories[currentCategoryIndex]['tips'] as List).length,
              (index) => _buildActionableTip(
                (_bmiCategories[currentCategoryIndex]['tips'] as List)[index],
                index + 1,
              ),
            ),
          ],
        ),
      ),
    );
  }
  
  Widget _buildActionableTip(String tip, int number) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Container(
            width: 24,
            height: 24,
            decoration: BoxDecoration(
              color: _bmiColor.withOpacity(0.2),
              shape: BoxShape.circle,
            ),
            child: Center(
              child: Text(
                '$number',
                style: TextStyle(
                  color: _bmiColor,
                  fontWeight: FontWeight.bold,
                  fontSize: 12,
                ),
              ),
            ),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: Text(
              tip,
              style: TextStyle(
                color: _isDarkMode ? Colors.grey.shade300 : Colors.grey.shade700,
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildBMICategories() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          'BMI Categories',
          style: TextStyle(
            fontSize: 22,
            fontWeight: FontWeight.bold,
            color: _isDarkMode ? Colors.white : Colors.blue.shade900,
          ),
        ),
        const SizedBox(height: 10),
        Text(
          'Understand what your BMI means',
          style: TextStyle(
            fontSize: 14,
            color: _isDarkMode ? Colors.grey.shade400 : Colors.grey.shade600,
          ),
        ),
        const SizedBox(height: 15),
        SizedBox(
          height: 140,
          child: ListView.builder(
            scrollDirection: Axis.horizontal,
            itemCount: _bmiCategories.length,
            itemBuilder: (context, index) {
              final category = _bmiCategories[index];
              final isCurrentCategory = category['category'] == _bmiCategory;
              
              return Container(
                width: 160,
                margin: EdgeInsets.only(
                  right: index < _bmiCategories.length - 1 ? 15 : 0,
                ),
                child: Card(
                  elevation: isCurrentCategory ? 6 : 4,
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(15),
                    side: isCurrentCategory 
                      ? BorderSide(color: category['color'], width: 2)
                      : BorderSide.none,
                  ),
                  color: _isDarkMode ? Colors.grey.shade800 : Colors.white,
                  child: Padding(
                    padding: const EdgeInsets.all(15.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Row(
                          children: [
                            Icon(
                              category['icon'],
                              color: category['color'],
                              size: 20,
                            ),
                            const SizedBox(width: 8),
                            Expanded(
                              child: Text(
                                category['category'],
                                style: TextStyle(
                                  fontWeight: FontWeight.bold,
                                  color: category['color'],
                                ),
                              ),
                            ),
                          ],
                        ),
                        const SizedBox(height: 10),
                        Text(
                          category['range'],
                          style: TextStyle(
                            fontSize: 14,
                            fontWeight: FontWeight.w600,
                            color: _isDarkMode ? Colors.grey.shade300 : Colors.grey.shade800,
                          ),
                        ),
                        const SizedBox(height: 5),
                        Text(
                          category['description'],
                          maxLines: 2,
                          overflow: TextOverflow.ellipsis,
                          style: TextStyle(
                            fontSize: 11,
                            color: _isDarkMode ? Colors.grey.shade500 : Colors.grey.shade600,
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              );
            },
          ),
        ),
      ],
    );
  }

  Widget _buildTipsSection() {
    return Card(
      elevation: 5,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(20),
      ),
      color: _isDarkMode ? Colors.grey.shade800 : Colors.white,
      child: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Icon(
                  Icons.health_and_safety,
                  color: _isDarkMode ? Colors.green.shade300 : Colors.green.shade700,
                ),
                const SizedBox(width: 10),
                Text(
                  'General Wellness Tips',
                  style: TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                    color: _isDarkMode ? Colors.white : Colors.green.shade900,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 15),
            Text(
              'Healthy habits for everyone:',
              style: TextStyle(
                color: _isDarkMode ? Colors.grey.shade400 : Colors.grey.shade700,
              ),
            ),
            const SizedBox(height: 10),
            // Tips dengan Grid
            GridView.builder(
              physics: const NeverScrollableScrollPhysics(),
              shrinkWrap: true,
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2,
                crossAxisSpacing: 12,
                mainAxisSpacing: 12,
                childAspectRatio: 1.2,
              ),
              itemCount: _generalTips.length,
              itemBuilder: (context, index) {
                final tip = _generalTips[index];
                return Container(
                  decoration: BoxDecoration(
                    color: _isDarkMode ? tip['color'].withOpacity(0.15) : tip['color'].withOpacity(0.1),
                    borderRadius: BorderRadius.circular(15),
                    border: Border.all(
                      color: tip['color'].withOpacity(0.3),
                      width: 1,
                    ),
                  ),
                  child: Padding(
                    padding: const EdgeInsets.all(12),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Icon(
                          tip['icon'],
                          color: tip['color'],
                          size: 24,
                        ),
                        const SizedBox(height: 8),
                        Text(
                          tip['title'],
                          style: TextStyle(
                            fontWeight: FontWeight.bold,
                            color: tip['color'],
                            fontSize: 14,
                          ),
                        ),
                        const SizedBox(height: 4),
                        Expanded(
                          child: Text(
                            tip['description'],
                            style: TextStyle(
                              fontSize: 11,
                              color: _isDarkMode ? Colors.grey.shade400 : Colors.grey.shade700,
                            ),
                            maxLines: 3,
                            overflow: TextOverflow.ellipsis,
                          ),
                        ),
                      ],
                    ),
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildResetButton() {
    return Center(
      child: SizedBox(
        width: double.infinity,
        child: ElevatedButton(
          onPressed: _resetValues,
          style: ElevatedButton.styleFrom(
            backgroundColor: _isDarkMode ? Colors.blue.shade800 : Colors.blue.shade700,
            foregroundColor: Colors.white,
            padding: const EdgeInsets.symmetric(vertical: 18),
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(15),
            ),
            elevation: 5,
          ),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(Icons.refresh),
              SizedBox(width: 10),
              Text(
                'Reset Values',
                style: TextStyle(
                  fontSize: 18,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

🧠 Penjelasan Struktur Kode

1. main() & MyApp

Fungsi main() adalah titik awal aplikasi. MaterialApp digunakan untuk mengatur tema dan halaman utama.

2. StatefulWidget

BMI Calculator menggunakan StatefulWidget karena nilainya berubah saat pengguna mengubah tinggi atau berat badan.

3. Perhitungan BMI

BMI dihitung setiap kali input berubah, lalu UI diperbarui secara real-time menggunakan setState().

4. UI Interaktif

Pada versi lengkap (seperti di demo), digunakan slider, animasi, dark mode, dan personal advice agar aplikasi terasa modern dan informatif.

✨ Penutup

Dengan Flutter dan Dart, konsep kesehatan seperti BMI bisa diubah menjadi aplikasi interaktif yang menarik, edukatif, dan mudah digunakan di berbagai platform.

Artikel ini menunjukkan bagaimana teori sederhana dapat diimplementasikan menjadi aplikasi nyata.

Komentar

Postingan populer dari blog ini

Kunci Jawaban Soal Pilihan Ganda Flutter UI (State Management)

🎮 Review Aplikasi Flutter: Pixel Quest To-Do