Mengelola State Flutter Tanpa Ribet: Bedah Kode Aplikasi BMI Calculator dengan Provider

Mengelola State Flutter Tanpa Ribet: Bedah Kode Aplikasi BMI Calculator dengan Provider



Bagi pengembang aplikasi Flutter, mengelola status (state management) sering kali menjadi tantangan tersendiri. Ketika aplikasi mulai tumbuh kompleks, meneruskan data antar-layar menggunakan constructor atau setState standar bisa membuat struktur kode menjadi rumit dan sulit dirawat.

Artikel ini membedah arsitektur kode aplikasi BMI Calculator sederhana yang ditulis menggunakan bahasa Dart dan framework Flutter, dengan memanfaatkan paket Provider sebagai solusi state management yang bersih dan efisien.

1. Menyiapkan Entry Point dan Provider Root

Langkah pertama dalam menggunakan provider adalah membungkus hierarki widget paling atas agar state dapat diakses oleh seluruh komponen di bawahnya.

Dart
void main() => runApp(const BMICalculatorApp());

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

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<BMIState>(
      create: (_) => BMIState(),
      builder: (context, _) => MaterialApp(
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          brightness: Brightness.dark,
          scaffoldBackgroundColor: const Color(0xFF0F111A),
          cardColor: const Color(0xFF1D1E33),
          primaryColor: const Color(0xFF536DFE),
          colorScheme: const ColorScheme.dark(
            primary: Color(0xFF536DFE),
            secondary: Color(0xFFFF4081),
          ),
        ),
        home: const BMIPage(),
      ),
    );
  }
}

Point Penting:

  • ChangeNotifierProvider: Berfungsi menginstansiasi BMIState dan menyediakannya untuk seluruh child widget.

  • Penerapan Tema Dark: Menggunakan warna-warna kustom seperti #0F111A (latar belakang) dan #1D1E33 (kartu) untuk memberikan tampilan modern bernuansa dark mode.

2. Memisahkan Logika Bisnis dengan ChangeNotifier

Kelas BMIState bertindak sebagai Model-View-ViewModel (MVVM) mini. Kelas ini menyimpan data, memproses perhitungan, dan memberi tahu UI jika terjadi perubahan data.

Dart
class BMIState extends ChangeNotifier {
  double _height = 170.0;
  double _weight = 60.0;

  double get height => _height;
  double get weight => _weight;

  set height(double value) {
    _height = value;
    notifyListeners(); // Memperbarui UI saat nilai berubah
  }

  set weight(double value) {
    _weight = value;
    notifyListeners();
  }

  // Rumus BMI: BB (kg) / (TB (m) * TB (m))
  double calculateBMI() => _weight / ((_height / 100) * (_height / 100));

  String getSuggestion(double bmi) {
    if (bmi < 18.5) {
      return "Recommendation: Focus on calorie-dense, nutrient-rich foods...";
    } else if (bmi < 25) {
      return "Recommendation: You have a healthy weight! Maintain your lifestyle...";
    } else {
      return "Recommendation: Consider increasing your intake of vegetables...";
    }
  }
}

Mengapa Pendekatan Ini Bagus?

  1. Dekonsentrasi Logika: Perhitungan matematika ($BMI = \frac{weight}{height^2}$) dan saran kesehatan terpisah penuh dari tampilan UI.

  2. notifyListeners(): Setiap kali slider digeser, metode setter dipanggil dan notifyListeners() akan memicu re-render pada widget yang mendengarkan (subscribe).

3. Konsumsi State di Tampilan Utama (BMIPage)

Pada layar utama, kita menggunakan Consumer<BMIState> agar hanya bagian yang membutuhkan data yang di-render ulang (rebuild) saat data berubah.

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("BMI CALCULATOR", style: TextStyle(letterSpacing: 1.2)),
        centerTitle: true,
        backgroundColor: Colors.transparent,
        elevation: 0,
      ),
      body: SafeArea(
        child: LayoutBuilder(
          builder: (context, constraints) {
            return SingleChildScrollView(
              padding: const EdgeInsets.all(24.0),
              child: ConstrainedBox(
                constraints: BoxConstraints(minHeight: constraints.maxHeight - 48),
                child: Consumer<BMIState>(
                  builder: (context, state, child) {
                    return Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        _SliderControl(
                          label: "HEIGHT (CM)",
                          value: state.height,
                          min: 100,
                          max: 220,
                          onChanged: (v) => state.height = v,
                        ),
                        const SizedBox(height: 24),
                        _SliderControl(
                          label: "WEIGHT (KG)",
                          value: state.weight,
                          min: 30,
                          max: 150,
                          onChanged: (v) => state.weight = v,
                        ),
                        const SizedBox(height: 32),
                        SizedBox(
                          width: double.infinity,
                          height: 65,
                          child: ElevatedButton(
                            style: ElevatedButton.styleFrom(
                              backgroundColor: Theme.of(context).colorScheme.primary,
                              shape: RoundedRectangleBorder(
                                  borderRadius: BorderRadius.circular(16)),
                            ),
                            onPressed: () => _showResult(context),
                            child: const Text("CALCULATE",
                                style: TextStyle(
                                    fontSize: 18, fontWeight: FontWeight.bold)),
                          ),
                        ),
                      ],
                    );
                  },
                ),
              ),
            );
          },
        ),
      ),
    );
  }

Sorotan Desain UI:

  • LayoutBuilder & SingleChildScrollView: Mencegah eror overflow (garis kuning-hitam) saat aplikasi dijalankan di perangkat dengan layar kecil atau orientasi lanskap.

  • Consumer<BMIState>: Menghubungkan slider tinggi dan berat badan langsung ke BMIState.

4. Menampilkan Hasil Menggunakan Bottom Sheet

Ketika tombol CALCULATE ditekan, fungsi _showResult membaca state terkini menggunakan Provider.of<BMIState>(context, listen: false) tanpa perlu melakukan render ulang pada layar utama.

Dart
  void _showResult(BuildContext context) {
    final state = Provider.of<BMIState>(context, listen: false);
    final bmi = state.calculateBMI();
    String status = bmi < 18.5 ? "Underweight" : bmi < 25 ? "Normal" : "Overweight";
    Color color = bmi < 18.5 ? Colors.blue : bmi < 25 ? Colors.green : Colors.redAccent;
    String suggestion = state.getSuggestion(bmi);

    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      backgroundColor: Colors.transparent,
      builder: (_) => Container(
        padding: const EdgeInsets.all(32),
        decoration: const BoxDecoration(
          color: Color(0xFF1D1E33),
          borderRadius: BorderRadius.vertical(top: Radius.circular(30)),
        ),
        child: SingleChildScrollView(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              const Text("BMI RESULT", style: TextStyle(fontSize: 20, color: Colors.white70)),
              const SizedBox(height: 16),
              Text(bmi.toStringAsFixed(1), style: const TextStyle(fontSize: 64, fontWeight: FontWeight.bold)),
              Text(status.toUpperCase(),
                  style: TextStyle(fontSize: 20, color: color, letterSpacing: 2, fontWeight: FontWeight.bold)),
              const SizedBox(height: 24),
              Text(suggestion,
                  textAlign: TextAlign.center,
                  style: const TextStyle(fontSize: 16, color: Colors.white54, height: 1.5)),
              const SizedBox(height: 24),
              ElevatedButton(
                  onPressed: () {
                    if (Navigator.of(context).mounted) {
                      Navigator.of(context).pop();
                    }
                  },
                  child: const Text("CLOSE"))
            ],
          ),
        ),
      ),
    );
  }
}

5. Komponen Reusable: Custom Slider Control

Untuk menjaga prinsip DRY (Don't Repeat Yourself), kontrol slider untuk tinggi dan berat badan diekstrak menjadi satu widget tersendiri yaitu _SliderControl.

Dart
class _SliderControl extends StatelessWidget {
  final String label;
  final double value;
  final double min;
  final double max;
  final ValueChanged<double> onChanged;

  const _SliderControl({
    required this.label,
    required this.value,
    required this.min,
    required this.max,
    required this.onChanged,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: Theme.of(context).cardColor,
        borderRadius: BorderRadius.circular(24),
      ),
      child: Column(
        children: [
          Text(label, style: const TextStyle(fontSize: 14, color: Colors.white70, letterSpacing: 1)),
          const SizedBox(height: 8),
          Text("${value.toInt()}", style: const TextStyle(fontSize: 40, fontWeight: FontWeight.w800)),
          Slider(
            value: value,
            min: min,
            max: max,
            activeColor: Theme.of(context).colorScheme.secondary,
            onChanged: onChanged,
          ),
        ],
      ),
    );
  }
}

Kesimpulan

Dengan menerapkan pola arsitektur ini, aplikasi tidak hanya memiliki tampilan visual yang rapi (dark theme modern), tetapi juga memiliki struktur kode yang:

  1. Modular: Widget UI dan logika bisnis terpisah dengan jelas.

  2. Efisien: Render ulang hanya terjadi pada komponen yang membutuhkan pembaruan data.

  3. Mudah Dirawat: Penambahan fitur baru (seperti pilihan jenis kelamin atau riwayat perhitungan) dapat dilakukan pada kelas BMIState tanpa merusak struktur UI yang ada.

Berikut adalah kode unit test sederhana menggunakan flutter_test untuk menguji logika bisnis pada kelas BMIState.

Pengujian ini berfokus pada dua aspek utama:

  1. Perhitungan nilai BMI sesuai dengan rumus $BMI = \frac{\text{weight}}{(\text{height}/100)^2}$.

  2. Output rekomendasi/saran berdasarkan kategori BMI (Underweight, Normal, dan Overweight).

Kode Unit Test (test/bmi_state_test.dart)

Dart
import 'package:flutter_test/flutter_test.dart';
// Sesuaikan import di bawah dengan lokasi file BMIState di proyek Anda
// import 'package:nama_project_anda/main.dart'; 

void main() {
  group('BMIState Business Logic Tests', () {
    late BMIState bmiState;

    setUp(() {
      // Inisialisasi ulang instance sebelum tiap skenario tes dijalankan
      bmiState = BMIState();
    });

    test('Nilai awal height dan weight harus sesuai default', () {
      expect(bmiState.height, 170.0);
      expect(bmiState.weight, 60.0);
    });

    test('Perhitungan BMI harus akurat', () {
      // Setup: Tinggi 170 cm, Berat 60 kg -> BMI ~ 20.76
      bmiState.height = 170.0;
      bmiState.weight = 60.0;

      final bmi = bmiState.calculateBMI();

      // Memastikan hasil perhitungan sesuai hingga 2 angka di belakang koma
      expect(bmi, closeTo(20.76, 0.01));
    });

    test('Memastikan setter memperbarui nilai dan memanggil notifyListeners', () {
      bool isNotified = false;
      bmiState.addListener(() {
        isNotified = true;
      });

      bmiState.height = 180.0;
      expect(bmiState.height, 180.0);
      expect(isNotified, isTrue);
    });

    group('Kategori dan Rekomendasi BMI', () {
      test('Memberikan rekomendasi yang tepat untuk Underweight (< 18.5)', () {
        bmiState.height = 180.0;
        bmiState.weight = 50.0; // BMI ~ 15.43
        
        final bmi = bmiState.calculateBMI();
        final suggestion = bmiState.getSuggestion(bmi);

        expect(bmi, lessThan(18.5));
        expect(suggestion, contains("Focus on calorie-dense"));
      });

      test('Memberikan rekomendasi yang tepat untuk Normal Weight (18.5 - 24.9)', () {
        bmiState.height = 170.0;
        bmiState.weight = 65.0; // BMI ~ 22.49

        final bmi = bmiState.calculateBMI();
        final suggestion = bmiState.getSuggestion(bmi);

        expect(bmi, greaterThanOrEqualTo(18.5));
        expect(bmi, lessThan(25.0));
        expect(suggestion, contains("You have a healthy weight!"));
      });

      test('Memberikan rekomendasi yang tepat untuk Overweight (>= 25)', () {
        bmiState.height = 160.0;
        bmiState.weight = 80.0; // BMI ~ 31.25

        final bmi = bmiState.calculateBMI();
        final suggestion = bmiState.getSuggestion(bmi);

        expect(bmi, greaterThanOrEqualTo(25.0));
        expect(suggestion, contains("Consider increasing your intake of vegetables"));
      });
    });
  });
}

Poin Penting dalam Unit Test Ini:

  1. closeTo() Matcher: Mengingat pembagian bilangan desimal (double) sering kali memiliki presisi mengambang, penggunaan closeTo(20.76, 0.01) memastikan tes tidak flaky (gagal akibat selisih desimal mikroskopis).

  2. Pengujian notifyListeners(): Memastikan bahwa pemanggilan setter height / weight benar-benar memicu notifyListeners(), yang penting untuk pembaruan UI pada Provider.

  3. group() Structure: Mengelompokkan skenario tes secara logis agar output laporan pengujian mudah dibaca di terminal.

Cara Menjalankan Tes

Jalankan perintah berikut di terminal proyek Flutter Anda:
Bash
flutter test test/bmi_state_test.dart
Share:

29 komentar:

  1. ijin copas postingan saya:
    Nama : Darsu
    Kelas : 11 PPLG 2
    link postingan :
    https://repositorydarsu.blogspot.com/2026/08/mengelola-state-flutter-tanpa-ribet.html

    BalasHapus
  2. izin copas postingan saya:
    Nama: Hadziq Nadhiril Afkar Haqiqi
    Kelas: 11 PPLG 2
    link postingan:
    https://bloghnah.blogspot.com/2026/08/bikin-aplikasi-bmi-calculator-kece.html

    BalasHapus
    Balasan
    1. terimakasih banyak atas link postingannya, semoga bermanfaaat.

      Hapus
  3. Ijin mengirimkan tugas 🫰
    Nama: Mohammad fathir mardiana
    Kelas: XI RPL 2
    https://www.blogger.com/blog/post/edit/2650898809176562963/7134441603511860584

    BalasHapus
  4. Komentar ini telah dihapus oleh pengarang.

    BalasHapus
  5. Ijin mengirimkan tugas pak🙏
    Nama: Muhammad Faiz Al-kautsar D
    Kelas: XI RPL 2
    Link Postingan:
    https://muhfaizal-kautsard.blogspot.com/2026/08/memahami-konsep-aplikasi-mobile-flutter.html

    BalasHapus
  6. Komentar ini telah dihapus oleh pengarang.

    BalasHapus
  7. izin copas postingan saya:
    Nama : Ilma Nur Ashfia
    Kelas : 11 PPLG 2
    link postingan :
    https://ilmanurashfiajames.blogspot.com/2026/08/membuat-aplikasi-bmi-kalkulator-dengan.html

    BalasHapus
  8. ijin mengirimkan tugas saya pak🙏
    Nama:Raffi Ardiansyah Ramadhan Hasibuan
    Kelas:XI RPL 2
    Link postingan:
    https://raffiblogger12.blogspot.com/2026/08/bmi-body-mass-index-pengertian-rumus.html

    BalasHapus
  9. Ijin copas postingan saya:
    Nama: Raditya alifanata sanjaya
    Kelas : XI RPL 2
    Link Postingan:
    https://radityaalfnta.blogspot.com/2026/08/modern-bmi-calculator-dengan-flutter.html

    BalasHapus
  10. Ijin copas postingan saya:
    Nama: Damar Aditya N
    Kelas : XI RPL 2
    Link Postingan:
    https://damaraditya15.blogspot.com/2026/08/bmi-cara-mudah-menghitung-dan.html

    BalasHapus
  11. ijin copas postingan saya:
    Nama: Revan zivilio m
    kelas : XI rpl 2
    https://banjarcity456.blogspot.com/2026/08/bmi-body-mass-index-pengertian-rumus.html

    BalasHapus
  12. Izin copas postingan saya:
    Nama: Faith Haziq SC
    Kelas: XI RPL 2
    Link: https://vantech7.blogspot.com/2026/08/cara-bikin-aplikasi-kalkulator-bmi.html

    BalasHapus
  13. Ijin copas posting saya:
    Nama: Rifqi Adi nur falah
    Kelas: XI rpl 2
    https://rifqiadinurfalah.blogspot.com/2026/08/membuat-aplikasi-bmi-calculator.html

    BalasHapus
  14. Izin copas postingan saya:
    Nama:Nurdin Abdul Jabar
    Kelas:XI RPL 2
    Link:
    https://nurdin13.blogspot.com/2026/08/membuat-aplikasi-kalkulator-bmi-dengan.html

    BalasHapus
  15. Ijin copas posting saya:
    Nama: Muhammad Arya putra
    Kelas: XI rpl 2
    https://kakalauza.blogspot.com/2026/08/bikin-aplikasi-bmi-calculator-kece.html

    BalasHapus
  16. Izin copas postingan saya:
    Nama: Rafi Nugraha
    Kelas: XI RPL 2
    Link: https://referensi-materi-mobile-development.blogspot.com/2026/08/kupas-tuntas-pembuatan-bmi-calculator.html

    BalasHapus
  17. izin copas postingan saya:
    Nama : Rifal ikhsan
    Kelas : XI RPL 2
    Link : https://rivalikhsann.blogspot.com/2026/08/bmi-body-mass-index-pengertian-rumus.html

    BalasHapus
  18. Komentar ini telah dihapus oleh pengarang.

    BalasHapus
  19. ijin copas postingan saya:
    Nama : Arjun Herdiana
    Kelas : XI RPL 2
    link postingan :https://arjunherdiana123.blogspot.com/2026/08/mengelola-state-flutter-tanpa-ribet.html

    BalasHapus
  20. ijin copas postingan saya
    Nama:Willi Wahyudi
    Kelas:XI RPL 2
    Link postingan: https://namawilli.blogspot.com/2026/08/cara-membuat-aplikasi-bmi-calculator.html

    BalasHapus
  21. Izin copas postingan ini
    Nama: Eka ramdani
    Kelas: IX rpl 2
    Link
    :https://ekaramdani90.blogspot.com/2026/08/membuat-aplikasi-bmi-calculator-di.html

    BalasHapus
  22. izin copas postingan ini
    Nama:Raya Septian Anjani
    Kelas:IX rpl 2
    Link
    :https://rayatokotnj.blogspot.com/2026/08/bmi-body-mass-index-pengertian-cara.html

    BalasHapus
  23. ijin copas postingan ini:
    Nama:Septi Nurmala Dewi
    Kelas:Xi RPL 2
    link
    https://septinurmaladewi.blogspot.com/2026/08/aplikasi-bmi-calculator-dengan-flutter.html?m=1

    BalasHapus
  24. ijin copas postingan ini
    Nama: Rizkya Amelia
    Kelas: XI RPL 2
    Link
    https://amelmel2102.blogspot.com/2026/08/membangun-aplikasi-bmi-menggunakan.html

    BalasHapus
  25. izin copas postingan ini
    nama: Ayu Setia Lestari
    kelas: XI RPL 2
    link
    https://ayusetialestari04.blogspot.com/2026/08/membuat-aplikasi-bmi-calculator-modern_0298608793.html

    BalasHapus
  26. izin mengirimkan tugas pak
    nama:Riska Julia Khaerani
    Kelas:XI RPL 2
    Link postingan:
    https://riskajuliakhaerani.blogspot.com/2026/08/membuat-aplikasi-bmi-sederhana-dengan.html

    BalasHapus
  27. Izin mengirimkan tugas Pak
    Nama: Bagus Dikri Maulana
    Kelas: XI RPL 2
    Link postingan:
    https://bagusdikri666.blogspot.com/2026/08/membuat-aplikasi-bmi-dengan-flutter.html

    BalasHapus

Chat Admin

Chat via WhatsApp

Categories

Blogger Tricks

Blogger Themes