Skip to content

Instantly share code, notes, and snippets.

@roipeker
Last active November 9, 2021 16:50
Show Gist options
  • Save roipeker/bbda71c985bb0c2b64a7c5d39d8b5bea to your computer and use it in GitHub Desktop.
Save roipeker/bbda71c985bb0c2b64a7c5d39d8b5bea to your computer and use it in GitHub Desktop.

Revisions

  1. roipeker revised this gist Sep 21, 2020. 1 changed file with 8 additions and 0 deletions.
    8 changes: 8 additions & 0 deletions pubspec.yaml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,8 @@
    # base dependencies.
    dependencies:
    flutter:
    sdk: flutter
    get: any
    flutter_icons: any
    google_fonts: any

  2. roipeker created this gist Sep 21, 2020.
    610 changes: 610 additions & 0 deletions bmi.dart
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,610 @@
    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),
    ],
    ),
    ),
    );
    }
    }