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 menginstansiasiBMIStatedan 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?
- Dekonsentrasi Logika: Perhitungan matematika ($BMI = \frac{weight}{height^2}$) dan saran kesehatan terpisah penuh dari tampilan UI.
notifyListeners(): Setiap kali slider digeser, metodesetterdipanggil dannotifyListeners()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 keBMIState.
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:
- Modular: Widget UI dan logika bisnis terpisah dengan jelas.
- Efisien: Render ulang hanya terjadi pada komponen yang membutuhkan pembaruan data.
- Mudah Dirawat: Penambahan fitur baru (seperti pilihan jenis kelamin atau riwayat perhitungan) dapat dilakukan pada kelas
BMIStatetanpa 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:
- Perhitungan nilai BMI sesuai dengan rumus $BMI = \frac{\text{weight}}{(\text{height}/100)^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:
closeTo()Matcher: Mengingat pembagian bilangan desimal (double) sering kali memiliki presisi mengambang, penggunaancloseTo(20.76, 0.01)memastikan tes tidak flaky (gagal akibat selisih desimal mikroskopis).- Pengujian
notifyListeners(): Memastikan bahwa pemanggilan setterheight/weightbenar-benar memicunotifyListeners(), yang penting untuk pembaruan UI padaProvider. 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







ijin copas postingan saya:
BalasHapusNama : Darsu
Kelas : 11 PPLG 2
link postingan :
https://repositorydarsu.blogspot.com/2026/08/mengelola-state-flutter-tanpa-ribet.html
izin copas postingan saya:
BalasHapusNama: Hadziq Nadhiril Afkar Haqiqi
Kelas: 11 PPLG 2
link postingan:
https://bloghnah.blogspot.com/2026/08/bikin-aplikasi-bmi-calculator-kece.html
terimakasih banyak atas link postingannya, semoga bermanfaaat.
Hapusbaik pak, terimakasih atas balasannya
HapusIjin mengirimkan tugas 🫰
BalasHapusNama: Mohammad fathir mardiana
Kelas: XI RPL 2
https://www.blogger.com/blog/post/edit/2650898809176562963/7134441603511860584
Komentar ini telah dihapus oleh pengarang.
BalasHapusIjin mengirimkan tugas pak🙏
BalasHapusNama: Muhammad Faiz Al-kautsar D
Kelas: XI RPL 2
Link Postingan:
https://muhfaizal-kautsard.blogspot.com/2026/08/memahami-konsep-aplikasi-mobile-flutter.html
Komentar ini telah dihapus oleh pengarang.
BalasHapusizin copas postingan saya:
BalasHapusNama : Ilma Nur Ashfia
Kelas : 11 PPLG 2
link postingan :
https://ilmanurashfiajames.blogspot.com/2026/08/membuat-aplikasi-bmi-kalkulator-dengan.html
ijin mengirimkan tugas saya pak🙏
BalasHapusNama:Raffi Ardiansyah Ramadhan Hasibuan
Kelas:XI RPL 2
Link postingan:
https://raffiblogger12.blogspot.com/2026/08/bmi-body-mass-index-pengertian-rumus.html
Ijin copas postingan saya:
BalasHapusNama: Raditya alifanata sanjaya
Kelas : XI RPL 2
Link Postingan:
https://radityaalfnta.blogspot.com/2026/08/modern-bmi-calculator-dengan-flutter.html
Ijin copas postingan saya:
BalasHapusNama: Damar Aditya N
Kelas : XI RPL 2
Link Postingan:
https://damaraditya15.blogspot.com/2026/08/bmi-cara-mudah-menghitung-dan.html
ijin copas postingan saya:
BalasHapusNama: Revan zivilio m
kelas : XI rpl 2
https://banjarcity456.blogspot.com/2026/08/bmi-body-mass-index-pengertian-rumus.html
Izin copas postingan saya:
BalasHapusNama: Faith Haziq SC
Kelas: XI RPL 2
Link: https://vantech7.blogspot.com/2026/08/cara-bikin-aplikasi-kalkulator-bmi.html
Ijin copas posting saya:
BalasHapusNama: Rifqi Adi nur falah
Kelas: XI rpl 2
https://rifqiadinurfalah.blogspot.com/2026/08/membuat-aplikasi-bmi-calculator.html
Izin copas postingan saya:
BalasHapusNama:Nurdin Abdul Jabar
Kelas:XI RPL 2
Link:
https://nurdin13.blogspot.com/2026/08/membuat-aplikasi-kalkulator-bmi-dengan.html
Ijin copas posting saya:
BalasHapusNama: Muhammad Arya putra
Kelas: XI rpl 2
https://kakalauza.blogspot.com/2026/08/bikin-aplikasi-bmi-calculator-kece.html
Izin copas postingan saya:
BalasHapusNama: Rafi Nugraha
Kelas: XI RPL 2
Link: https://referensi-materi-mobile-development.blogspot.com/2026/08/kupas-tuntas-pembuatan-bmi-calculator.html
izin copas postingan saya:
BalasHapusNama : Rifal ikhsan
Kelas : XI RPL 2
Link : https://rivalikhsann.blogspot.com/2026/08/bmi-body-mass-index-pengertian-rumus.html
Komentar ini telah dihapus oleh pengarang.
BalasHapusijin copas postingan saya:
BalasHapusNama : Arjun Herdiana
Kelas : XI RPL 2
link postingan :https://arjunherdiana123.blogspot.com/2026/08/mengelola-state-flutter-tanpa-ribet.html
ijin copas postingan saya
BalasHapusNama:Willi Wahyudi
Kelas:XI RPL 2
Link postingan: https://namawilli.blogspot.com/2026/08/cara-membuat-aplikasi-bmi-calculator.html
Izin copas postingan ini
BalasHapusNama: Eka ramdani
Kelas: IX rpl 2
Link
:https://ekaramdani90.blogspot.com/2026/08/membuat-aplikasi-bmi-calculator-di.html
izin copas postingan ini
BalasHapusNama:Raya Septian Anjani
Kelas:IX rpl 2
Link
:https://rayatokotnj.blogspot.com/2026/08/bmi-body-mass-index-pengertian-cara.html
ijin copas postingan ini:
BalasHapusNama:Septi Nurmala Dewi
Kelas:Xi RPL 2
link
https://septinurmaladewi.blogspot.com/2026/08/aplikasi-bmi-calculator-dengan-flutter.html?m=1
ijin copas postingan ini
BalasHapusNama: Rizkya Amelia
Kelas: XI RPL 2
Link
https://amelmel2102.blogspot.com/2026/08/membangun-aplikasi-bmi-menggunakan.html
izin copas postingan ini
BalasHapusnama: Ayu Setia Lestari
kelas: XI RPL 2
link
https://ayusetialestari04.blogspot.com/2026/08/membuat-aplikasi-bmi-calculator-modern_0298608793.html
izin mengirimkan tugas pak
BalasHapusnama:Riska Julia Khaerani
Kelas:XI RPL 2
Link postingan:
https://riskajuliakhaerani.blogspot.com/2026/08/membuat-aplikasi-bmi-sederhana-dengan.html
Izin mengirimkan tugas Pak
BalasHapusNama: Bagus Dikri Maulana
Kelas: XI RPL 2
Link postingan:
https://bagusdikri666.blogspot.com/2026/08/membuat-aplikasi-bmi-dengan-flutter.html