Skip to content

Instantly share code, notes, and snippets.

@fer-ri
Forked from roipeker/bmi.dart
Created September 25, 2020 03:55
Show Gist options
  • Save fer-ri/a5eb484e613d3d032f8df826940cbc7e to your computer and use it in GitHub Desktop.
Save fer-ri/a5eb484e613d3d032f8df826940cbc7e to your computer and use it in GitHub Desktop.
BMI Calculator clone with GetX (+ Theme modes)
import 'dart:async';
import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_icons/flutter_icons.dart';
import 'package:get/get.dart';
import 'package:google_fonts/google_fonts.dart';
void main() {
SystemChrome.setSystemUIOverlayStyle(Styles.baseSystemUIStyle);
runApp(BMIApp());
}
enum Gender { female, male }
abstract class Styles {
static const primaryColor = Color(0xFFEB1555);
static const backgroundColor = Color(0xFF0A0D22);
static var lighTheme = ThemeData.light().copyWith(
primaryColor: primaryColor,
colorScheme: ColorScheme.light(primary: primaryColor),
scaffoldBackgroundColor: Colors.grey.shade300,
);
static var darkTheme = ThemeData.dark().copyWith(
primaryColor: primaryColor,
colorScheme: ColorScheme.dark(primary: primaryColor),
scaffoldBackgroundColor: Styles.backgroundColor,
);
static SystemUiOverlayStyle baseSystemUIStyle =
SystemUiOverlayStyle.dark.copyWith(
statusBarColor: Colors.transparent,
systemNavigationBarColor: Styles.primaryColor,
);
static Color get iconColor {
return Get.isDarkMode ? Colors.white : Colors.redAccent;
}
static Color get textColor {
return Get.isDarkMode ? Colors.white : Color(0xff656565);
}
static final bigValueTextStyle = GoogleFonts.nunitoSans(
fontWeight: FontWeight.w800,
color: Colors.white,
fontSize: 46,
fontFeatures: [
FontFeature.proportionalFigures(),
FontFeature.tabularFigures()
],
);
static const yourResultTextStyle =
TextStyle(fontSize: 44, fontWeight: FontWeight.w600, color: Colors.white);
static const resultStatusTextStyle =
TextStyle(fontWeight: FontWeight.bold, fontSize: 22);
static const cmTextStyle =
TextStyle(fontWeight: FontWeight.w200, fontSize: 12);
static const resultPointsTextStyle =
TextStyle(fontWeight: FontWeight.w800, color: Colors.white, fontSize: 90);
static const resultTextStyle = TextStyle(
fontWeight: FontWeight.normal, color: Colors.white, fontSize: 22);
static const cardLabelTextStyle =
TextStyle(fontWeight: FontWeight.w300, color: Colors.white54);
}
class BMIApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
title: 'BMI calculator (GetX)',
theme: Styles.lighTheme,
darkTheme: Styles.darkTheme,
themeMode: ThemeMode.dark,
initialBinding: BindingsBuilder(() {
Get.put(BMIHomeController());
}),
home: BMIHome(),
);
}
}
enum BMIStatus { overweight, normal, underweight }
class BmiCalculator {
BMIStatus status = BMIStatus.normal;
double points = 0;
String text = 'You have a higher than normal';
double _bmi = 0;
void calculateBMI({double weight, double height}) {
_bmi = weight / math.pow(height / 100, 2);
points = _bmi;
// update values.
if (_bmi >= 25) {
status = BMIStatus.overweight;
text = 'You have a higher than normal body weight. Try to exercise more';
} else if (_bmi > 18.5) {
status = BMIStatus.normal;
text = 'You have a normal body weight. Good job!';
} else {
status = BMIStatus.underweight;
text =
'You have a lower than normal body weight. You can eat a bit more.';
}
}
}
class BMIHomeController extends GetxController {
// result status colors
static const _bmiStatusColors = {
BMIStatus.overweight: Colors.redAccent,
BMIStatus.normal: Colors.lightGreenAccent,
BMIStatus.underweight: Colors.yellow,
};
final gender = Gender.female.obs;
final height = 180.0.obs;
final minHeight = 120.0;
final maxHeight = 240.0;
final weight = 85.obs;
final age = 19.obs;
final _calculator = BmiCalculator();
Color get resultStatusColor => _bmiStatusColors[_calculator.status];
String get resultStatusTitle =>
describeEnum(_calculator.status).toUpperCase();
get resultText => _calculator.text;
get resultPointsString => _calculator.points.toStringAsFixed(1);
@override
void onInit() {
_constrainValue(weight, min: 10);
_constrainValue(age, min: 18);
}
void _constrainValue(RxInt value, {int min = 0}) {
ever(value, (_) {
value.value = min;
}, condition: () => value.value < min);
}
double getGenderOpacity(Gender value) => gender.value == value ? 1 : .25;
String get heightString => height.value.toStringAsFixed(1);
void handleCalculateTap() {
_calculator.calculateBMI(
height: height.value,
weight: weight.value.toDouble(),
);
Get.to(BMIResult());
}
void handleReCalculateTap() => Get.back();
void toggleTheme() {
Get.changeThemeMode(Get.isDarkMode ? ThemeMode.light : ThemeMode.dark);
}
Timer _buttonTicker;
/// custom timer ticker
void handleTickButton(RxInt value, bool isPressed, int dir) {
if (isPressed) {
int countdownWait = 8;
int waitTicker = 0;
_buttonTicker = Timer.periodic(48.milliseconds, (_) {
if (countdownWait <= 0 || ++waitTicker % countdownWait == 0) {
countdownWait--;
value.value += dir;
}
});
} else {
_buttonTicker?.cancel();
value.value += dir;
}
}
}
class BMIAppbar extends StatelessWidget implements PreferredSizeWidget {
final bool centerTitle;
const BMIAppbar({Key key, this.centerTitle = false}) : super(key: key);
@override
Widget build(BuildContext context) {
context.theme;
final BMIHomeController controller = Get.find();
final canPop = Navigator.canPop(context);
return AppBar(
brightness: Get.isDarkMode ? Brightness.dark : Brightness.light,
backgroundColor: Colors.transparent,
leading: Visibility(
visible: canPop,
child: BackButton(
color: Styles.iconColor,
onPressed: Get.back,
),
),
elevation: 0,
title: Text(
'BMI CALCULATOR',
style: TextStyle(color: Styles.textColor),
),
centerTitle: centerTitle,
actions: [
CupertinoButton(
child: Icon(
Get.isDarkMode ? Icons.brightness_2 : Icons.brightness_2_outlined,
size: 16),
onPressed: controller.toggleTheme,
),
],
);
}
@override
Size get preferredSize => Size.fromHeight(kToolbarHeight);
}
class BMIResult extends GetView<BMIHomeController> {
@override
Widget build(BuildContext context) {
context.theme;
return Scaffold(
appBar: BMIAppbar(
centerTitle: false,
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Your Result',
style: Styles.yourResultTextStyle.copyWith(
color: Styles.textColor,
),
).paddingSymmetric(vertical: 12, horizontal: 12),
Expanded(
child: AppCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(
controller.resultStatusTitle,
style: Styles.resultStatusTextStyle.copyWith(
color: controller.resultStatusColor,
),
),
Text(
controller.resultPointsString,
style: Styles.resultPointsTextStyle.copyWith(
color: Styles.textColor,
),
),
Text(
controller.resultText,
style: Styles.resultTextStyle.copyWith(
color: Styles.textColor,
),
textAlign: TextAlign.center,
),
],
).paddingAll(12),
).paddingSymmetric(vertical: 12, horizontal: 12),
),
AppMainButton(
label: 'RE-CALCULATE',
onTap: controller.handleReCalculateTap,
),
],
));
}
}
class BMIHome extends GetView<BMIHomeController> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: BMIAppbar(centerTitle: true),
body: Column(
children: [
Expanded(child: GenderSelector()),
Expanded(child: HeightPane()),
Expanded(child: WeightSelector().paddingOnly(bottom: 0)),
AppMainButton(
label: 'CALCULATE',
onTap: controller.handleCalculateTap,
),
],
),
);
}
}
class CardLabel extends StatelessWidget {
final String text;
final double size;
const CardLabel(this.text, {this.size = 16, Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
context.theme;
return Text(
text,
style: Styles.cardLabelTextStyle.copyWith(
fontSize: size,
color: Styles.textColor.withOpacity(.54),
),
);
}
}
class WeightSelector extends GetView<BMIHomeController> {
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: CounterPane(
label: 'WEIGHT',
onButtonState: (state, dir) =>
controller.handleTickButton(controller.weight, state, dir),
realValue: controller.weight,
).paddingAll(12)),
Expanded(
child: CounterPane(
label: 'AGE',
onButtonState: (state, dir) =>
controller.handleTickButton(controller.age, state, dir),
realValue: controller.age,
).paddingAll(12)),
],
);
}
}
class CounterPane extends StatelessWidget {
final String label;
final RxInt realValue;
final Function(bool, int) onButtonState;
const CounterPane({
Key key,
this.label,
this.onButtonState,
this.realValue,
}) : super(key: key);
@override
Widget build(BuildContext context) {
context.theme;
return AppCard(
padding: EdgeInsets.all(4),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CardLabel(label),
Obx(
() => Text(
'$realValue',
style: Styles.bigValueTextStyle.copyWith(color: Styles.textColor),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AppIconButton.more(
onHighlight: onButtonState,
),
SizedBox(width: 8),
AppIconButton.less(
onHighlight: onButtonState,
// onTap: () => realValue(realValue() - 1),
),
],
)
],
),
);
}
}
class AppIconButton extends StatelessWidget {
final Function(int) onTap;
final Function(bool, int) onHighlight;
final IconData iconData;
final int dir;
const AppIconButton(
{Key key, this.iconData, this.onTap, this.onHighlight, this.dir = 0})
: super(key: key);
AppIconButton.more({this.onTap, this.onHighlight})
: iconData = Icons.add,
dir = 1,
super();
AppIconButton.less({this.onTap, this.onHighlight})
: iconData = Icons.remove,
dir = -1,
super();
@override
Widget build(BuildContext context) {
context.theme;
return FlatButton(
visualDensity: VisualDensity.compact,
shape: const CircleBorder(),
minWidth: 48,
height: 54,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
// padding: EdgeInsets.zero,
child: Icon(
iconData,
size: 34,
color: Styles.iconColor,
),
onHighlightChanged: (flag) => onHighlight?.call(flag, dir),
onPressed: () => onTap?.call(dir),
color: Styles.iconColor.withOpacity(.12),
);
}
}
class HeightPane extends GetView<BMIHomeController> {
@override
Widget build(BuildContext context) {
context.theme;
return AppCard(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(child: CardLabel('HEIGHT')),
Center(
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.baseline,
children: [
Obx(
() => Text(
controller.heightString,
textAlign: TextAlign.right,
style: Styles.bigValueTextStyle
.copyWith(color: Styles.textColor),
),
),
Text(' cm', style: Styles.cmTextStyle),
],
),
),
Obx(
() => Slider.adaptive(
min: controller.minHeight,
max: controller.maxHeight,
value: controller.height(),
onChanged: controller.height,
activeColor: Styles.primaryColor,
// activeColor: Styles.textColor,
// thumbColor: Get.theme.primaryColor,
),
),
],
),
).paddingAll(12);
}
}
class GenderSelector extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(child: AppToggle.male().paddingAll(12)),
Expanded(child: AppToggle.female().paddingAll(12)),
],
);
}
}
class AppMainButton extends StatelessWidget {
final VoidCallback onTap;
final String label;
const AppMainButton({Key key, this.onTap, this.label}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
height: 70,
child: FlatButton(
color: Get.theme.primaryColor,
onPressed: onTap,
shape: const ContinuousRectangleBorder(),
child: Text(
label,
style: Get.textTheme.headline5.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
);
}
}
class AppCard extends StatelessWidget {
final Widget child;
final EdgeInsets padding;
const AppCard({
Key key,
this.child,
this.padding,
}) : super(key: key);
@override
Widget build(BuildContext context) {
context.theme;
return Container(
padding: padding,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
// color: Colors.white.withOpacity(.06),
color: Styles.textColor.withOpacity(.06),
),
child: child,
);
}
}
class AppToggle extends GetView<BMIHomeController> {
final String label;
final Widget icon;
final Gender gender;
const AppToggle({Key key, this.label, this.icon, this.gender})
: super(key: key);
AppToggle.male()
: label = 'MALE',
gender = Gender.male,
icon = Icon(Ionicons.md_male),
super();
AppToggle.female()
: label = 'FEMALE',
gender = Gender.female,
icon = Icon(Ionicons.md_female),
super();
@override
Widget build(BuildContext context) {
context.theme;
return AppCard(
child: FlatButton(
onPressed: () => controller.gender(gender),
padding: EdgeInsets.all(12),
shape: const ContinuousRectangleBorder(),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: FittedBox(
child: Obx(
() => Opacity(
opacity: controller.getGenderOpacity(gender),
child: IconTheme(
data: IconThemeData(color: Styles.iconColor),
child: icon,
),
),
),
),
),
SizedBox(height: 12),
CardLabel(label),
],
),
),
);
}
}
# base dependencies.
dependencies:
flutter:
sdk: flutter
get: any
flutter_icons: any
google_fonts: any
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment