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 { @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 { @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 { @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 { @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 { 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), ], ), ), ); } }