diff --git a/bmi_calculator/.gitignore b/bmi_calculator/.gitignore new file mode 100644 index 00000000..55659b7e --- /dev/null +++ b/bmi_calculator/.gitignore @@ -0,0 +1,52 @@ +.DS_Store +.dart_tool/ + +.vscode/ + +.packages +.pub/ + +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +migrate_working_dir/ + +*.swp +profile + +DerivedData/ + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +build/ +.android/ +.ios/ +.flutter-plugins +.flutter-plugins-dependencies + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/bmi_calculator/.metadata b/bmi_calculator/.metadata new file mode 100644 index 00000000..74dfa134 --- /dev/null +++ b/bmi_calculator/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + channel: stable + +project_type: module diff --git a/bmi_calculator/README.md b/bmi_calculator/README.md new file mode 100644 index 00000000..19504a8f --- /dev/null +++ b/bmi_calculator/README.md @@ -0,0 +1,7 @@ +# bmi_calculator + +A person’s Body-Mass Index, or BMI, helps them check whether they’re a healthy weight for their height. If a person weighs less or more than the recommended weight for their height, it could lead to health issues in the future. While BMI is not the only factor individuals should consider while working on their health and fitness, it is a good starting point. To understand what your BMI is, you need to know your height and weight. You can then use an online BMI calculator to check your BMI, which will help you understand if you’re underweight, a healthy weight, overweight or obese. Or, you can measure your height in metres and weight in kilograms. Divide your weight by your height squared to calculate your BMI. + +BMI Calculator [Wiki](https://en.wikipedia.org/wiki/Body_mass_index) + + diff --git a/bmi_calculator/analysis_options.yaml b/bmi_calculator/analysis_options.yaml new file mode 100644 index 00000000..a5744c1c --- /dev/null +++ b/bmi_calculator/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/bmi_calculator/lib/bmi_calculator.dart b/bmi_calculator/lib/bmi_calculator.dart new file mode 100644 index 00000000..0f137b43 --- /dev/null +++ b/bmi_calculator/lib/bmi_calculator.dart @@ -0,0 +1,9 @@ +import 'dart:math'; + +import 'package:bmi_calculator/body_model.dart'; + +// I've used weight/height^2 (kg/m^2) +// You can expand this logic +double calculateBMI({required BodyModel bodyModel}) { + return (bodyModel.weight) / pow(bodyModel.height / 100, 2); +} diff --git a/bmi_calculator/lib/body_model.dart b/bmi_calculator/lib/body_model.dart new file mode 100644 index 00000000..710cae76 --- /dev/null +++ b/bmi_calculator/lib/body_model.dart @@ -0,0 +1,19 @@ +enum Sex { + male, + female, +} + +// User body model class +class BodyModel { + Sex sex; + int height; + int weight; + int age; + + BodyModel({ + required this.sex, + required this.height, + required this.weight, + required this.age, + }); +} diff --git a/bmi_calculator/lib/calculator/calculate_button.dart b/bmi_calculator/lib/calculator/calculate_button.dart new file mode 100644 index 00000000..54249f27 --- /dev/null +++ b/bmi_calculator/lib/calculator/calculate_button.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import '../palette.dart'; + +class CalculateButton extends StatelessWidget { + const CalculateButton({ + super.key, + required this.onTap, + }); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Container( + height: MediaQuery.of(context).size.height / 12, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: Palette.action, + ), + child: TextButton( + style: ButtonStyle( + overlayColor: + MaterialStateProperty.all(Colors.white.withOpacity(0.10)), + ), + onPressed: onTap, + child: const Text( + 'CALCULATE YOUR BMI', + style: TextStyle( + color: Palette.textActive, + fontSize: 20, + ), + ), + ), + ); + } +} diff --git a/bmi_calculator/lib/calculator/calculator_body.dart b/bmi_calculator/lib/calculator/calculator_body.dart new file mode 100644 index 00000000..93f1e44e --- /dev/null +++ b/bmi_calculator/lib/calculator/calculator_body.dart @@ -0,0 +1,105 @@ +import 'package:bmi_calculator/calculator/calculate_button.dart'; +import 'package:flutter/material.dart'; +import 'package:bmi_calculator/calculator/height_widget.dart'; +import 'package:bmi_calculator/calculator/int_picker_widget.dart'; +import 'package:bmi_calculator/calculator/sex_widget.dart'; +import 'package:bmi_calculator/result/result_page.dart'; +import 'package:bmi_calculator/body_model.dart'; +import 'package:bmi_calculator/palette.dart'; + +class CalculatorBody extends StatefulWidget { + const CalculatorBody({ + super.key, + required this.model, + }); + + final BodyModel model; + + @override + State createState() => _CalculatorBodyState(); +} + +class _CalculatorBodyState extends State { + @override + Widget build(BuildContext context) { + return Container( + color: Palette.background, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + SexWidget( + sex: widget.model.sex, + onMaleTap: () => setState(() { + // Set sex to male + widget.model.sex = Sex.male; + }), + onFemaleTap: () => setState(() { + // Set sex to female + widget.model.sex = Sex.female; + })), + HeightWidget( + height: widget.model.height, + onHeightChanged: (height) => setState(() { + // Set height and round to Int + widget.model.height = height.toInt(); + }), + ), + SizedBox( + height: (MediaQuery.of(context).size.width - 48) / 2, + child: Row( + children: [ + Expanded( + // Weight widget + child: IntPickerWidget( + title: 'Weight', + onIncreaseTap: () => setState(() { + // Increase weight + widget.model.weight++; + }), + onDecreaseTap: () => setState(() { + // Decrease weight + widget.model.weight--; + }), + value: widget.model.weight, + ), + ), + const SizedBox( + width: 5, + ), + Expanded( + // Age widget + child: IntPickerWidget( + title: 'Age', + onIncreaseTap: () => setState(() { + // Increase age + widget.model.age++; + }), + onDecreaseTap: () => setState(() { + // Decrease age + widget.model.age--; + }), + value: widget.model.age, + ), + ) + ], + ), + ), + CalculateButton( + onTap: () { + // Navigate to Result Page + Navigator.push( + context, + MaterialPageRoute( + builder: ((context) => ResultPage(model: widget.model)), + ), + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/bmi_calculator/lib/calculator/calculator_page.dart b/bmi_calculator/lib/calculator/calculator_page.dart new file mode 100644 index 00000000..c811323e --- /dev/null +++ b/bmi_calculator/lib/calculator/calculator_page.dart @@ -0,0 +1,31 @@ +import 'package:bmi_calculator/calculator/calculator_body.dart'; +import 'package:flutter/material.dart'; +import 'package:bmi_calculator/body_model.dart'; +import 'package:bmi_calculator/palette.dart'; + +class CalculatorPage extends StatelessWidget { + CalculatorPage({ + Key? key, + required this.title, + }) : super(key: key); + + final String title; + + final BodyModel model = BodyModel( + sex: Sex.male, + height: 183, + weight: 74, + age: 19, + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(title), + backgroundColor: Palette.background, + ), + body: CalculatorBody(model: model), + ); + } +} diff --git a/bmi_calculator/lib/calculator/height_widget.dart b/bmi_calculator/lib/calculator/height_widget.dart new file mode 100644 index 00000000..bf5a8ccb --- /dev/null +++ b/bmi_calculator/lib/calculator/height_widget.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; + +import '../palette.dart'; + +class HeightWidget extends StatelessWidget { + const HeightWidget({ + super.key, + required this.height, + required this.onHeightChanged, + }); + + final int height; + final Function(double) onHeightChanged; + + @override + Widget build(BuildContext context) { + return Container( + height: (MediaQuery.of(context).size.width - 48) / 2, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: Palette.cardBackgroundInactive, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'HEIGHT', + style: TextStyle( + fontSize: 23, + fontWeight: FontWeight.w600, + color: Palette.textInactive, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text( + height.round().toString(), + style: const TextStyle( + fontSize: 50, + fontWeight: FontWeight.w800, + color: Palette.textActive, + ), + ), + const Text( + 'cm', + style: TextStyle( + fontSize: 30, + color: Palette.textInactive, + ), + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24.0, + ), + child: SliderTheme( + data: SliderTheme.of(context).copyWith( + trackHeight: 1.0, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 15, + ), + ), + child: Slider( + value: height.toDouble(), + min: 150.0, + max: 210.0, + activeColor: Palette.textActive, + inactiveColor: Palette.textInactive, + thumbColor: Palette.action, + onChanged: onHeightChanged, + ), + ), + ), + ], + ), + ); + } +} diff --git a/bmi_calculator/lib/calculator/int_picker_widget.dart b/bmi_calculator/lib/calculator/int_picker_widget.dart new file mode 100644 index 00000000..c0a5aa6e --- /dev/null +++ b/bmi_calculator/lib/calculator/int_picker_widget.dart @@ -0,0 +1,84 @@ +import 'package:bmi_calculator/palette.dart'; +import 'package:flutter/material.dart'; + +class IntPickerWidget extends StatelessWidget { + const IntPickerWidget({ + super.key, + required this.title, + required this.onIncreaseTap, + required this.onDecreaseTap, + required this.value, + }); + + final String title; + final VoidCallback onIncreaseTap; + final VoidCallback onDecreaseTap; + final int value; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: Palette.cardBackgroundInactive, + ), + height: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + title.toUpperCase(), + style: const TextStyle( + fontSize: 23, + fontWeight: FontWeight.w600, + color: Palette.textInactive, + ), + ), + Text( + value.toString(), + style: const TextStyle( + fontSize: 50, + fontWeight: FontWeight.w800, + color: Palette.textActive, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + InkWell( + onTap: onDecreaseTap, + child: Container( + width: 56, + height: 56, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Palette.cardBackgroundActive, + ), + child: const Icon( + Icons.remove, + color: Palette.textActive, + ), + ), + ), + InkWell( + onTap: onIncreaseTap, + child: Container( + width: 56, + height: 56, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Palette.cardBackgroundActive, + ), + child: const Icon( + Icons.add, + color: Palette.textActive, + ), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/bmi_calculator/lib/calculator/sex_widget.dart b/bmi_calculator/lib/calculator/sex_widget.dart new file mode 100644 index 00000000..8d85e97c --- /dev/null +++ b/bmi_calculator/lib/calculator/sex_widget.dart @@ -0,0 +1,103 @@ +import 'package:bmi_calculator/palette.dart'; +import 'package:flutter/material.dart'; + +import '../body_model.dart'; + +class SexWidget extends StatelessWidget { + const SexWidget({ + super.key, + required this.sex, + required this.onMaleTap, + required this.onFemaleTap, + }); + + final Sex sex; + final VoidCallback onMaleTap; + final VoidCallback onFemaleTap; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: (MediaQuery.of(context).size.width - 48) / 2, + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: onMaleTap, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: sex == Sex.male + ? Palette.cardBackgroundActive + : Palette.cardBackgroundInactive, + ), + height: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.male_rounded, + size: 100, + color: sex == Sex.male + ? Palette.textActive + : Palette.textInactive, + ), + Text( + 'MALE', + style: TextStyle( + fontSize: 23, + fontWeight: FontWeight.w600, + color: sex == Sex.male + ? Palette.textActive + : Palette.textInactive, + ), + ), + ], + ), + ), + ), + ), + const SizedBox( + width: 5, + ), + Expanded( + child: GestureDetector( + onTap: onFemaleTap, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: sex == Sex.female + ? Palette.cardBackgroundActive + : Palette.cardBackgroundInactive, + ), + height: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.female_rounded, + size: 100, + color: sex == Sex.female + ? Palette.textActive + : Palette.textInactive, + ), + Text( + 'FEMALE', + style: TextStyle( + fontSize: 23, + fontWeight: FontWeight.w600, + color: sex == Sex.female + ? Palette.textActive + : Palette.textInactive, + ), + ), + ], + ), + ), + ), + ) + ], + ), + ); + } +} diff --git a/bmi_calculator/lib/main.dart b/bmi_calculator/lib/main.dart new file mode 100644 index 00000000..cf7f056f --- /dev/null +++ b/bmi_calculator/lib/main.dart @@ -0,0 +1,26 @@ +import 'package:bmi_calculator/calculator/calculator_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + // Prevent landscape orientation + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + ]); + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'BMI Calculator', + home: CalculatorPage(title: 'BMI CALCULATOR'), + ); + } +} diff --git a/bmi_calculator/lib/palette.dart b/bmi_calculator/lib/palette.dart new file mode 100644 index 00000000..724a7f62 --- /dev/null +++ b/bmi_calculator/lib/palette.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +// Color constants +class Palette { + static const background = Color(0xFF0A0D22); + static const cardBackgroundInactive = Color(0xFF121428); + static const cardBackgroundActive = Color(0xFF1D1F33); + static const textInactive = Color(0xFF8D8E99); + static const textActive = Color(0xFFFFFFFF); + static const action = Color(0xFFEC1554); + static const normalResult = Color(0xFF4BE57A); + static const underweightResult = Color(0xFFE5D94B); + static const overWeightResult = Color(0xFFE54B4B); +} diff --git a/bmi_calculator/lib/result/result_body_widget.dart b/bmi_calculator/lib/result/result_body_widget.dart new file mode 100644 index 00000000..07fe2e88 --- /dev/null +++ b/bmi_calculator/lib/result/result_body_widget.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +import '../palette.dart'; +import 'result_content_widget.dart'; + +class ResultBodyWidget extends StatelessWidget { + const ResultBodyWidget({ + super.key, + required this.result, + }); + + final double result; + + @override + Widget build(BuildContext context) { + return Container( + color: Palette.background, + width: double.infinity, + child: Padding( + padding: const EdgeInsets.all(72.0), + child: ResultContentWidget(result: result), + ), + ); + } +} diff --git a/bmi_calculator/lib/result/result_content_widget.dart b/bmi_calculator/lib/result/result_content_widget.dart new file mode 100644 index 00000000..5f700ea4 --- /dev/null +++ b/bmi_calculator/lib/result/result_content_widget.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; + +import '../palette.dart'; + +class ResultContentWidget extends StatelessWidget { + const ResultContentWidget({ + super.key, + required this.result, + }); + + final double result; + + Color resultColor() { + if (result < 18.5) { + return Palette.underweightResult; + } else if (result > 25) { + return Palette.overWeightResult; + } else { + return Palette.normalResult; + } + } + + String diagnosis() { + if (result < 18.5) { + return 'UNDERWEIGHT'; + } else if (result > 25) { + return 'OVERWEIGHT'; + } else { + return 'NORMAL'; + } + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + diagnosis(), + style: TextStyle( + color: resultColor(), + fontWeight: FontWeight.w600, + fontSize: 25, + ), + ), + Text( + // [toStringAsFixed] for decimal-point string-representation + result.toStringAsFixed(1), + style: const TextStyle( + color: Palette.textActive, + fontWeight: FontWeight.w600, + fontSize: 100, + ), + ), + const Column( + children: [ + Text( + 'Normal BMI range:', + style: TextStyle( + color: Palette.textInactive, + fontWeight: FontWeight.w500, + fontSize: 20, + ), + ), + SizedBox(height: 8), + Text( + '18.5 - 25 kg/m2', + style: TextStyle( + color: Palette.textActive, + fontWeight: FontWeight.w500, + fontSize: 20, + ), + ), + ], + ), + ], + ); + } +} diff --git a/bmi_calculator/lib/result/result_page.dart b/bmi_calculator/lib/result/result_page.dart new file mode 100644 index 00000000..2ddab74b --- /dev/null +++ b/bmi_calculator/lib/result/result_page.dart @@ -0,0 +1,35 @@ +import 'package:bmi_calculator/body_model.dart'; +import 'package:bmi_calculator/palette.dart'; +import 'package:bmi_calculator/result/result_body_widget.dart'; +import 'package:flutter/material.dart'; + +import '../bmi_calculator.dart'; + +class ResultPage extends StatelessWidget { + final BodyModel model; + + const ResultPage({ + super.key, + required this.model, + }); + + @override + Widget build(BuildContext context) { + final result = calculateBMI(bodyModel: model); + + return Scaffold( + appBar: AppBar( + title: const Text('RESULT'), + backgroundColor: Palette.background, + leading: IconButton( + onPressed: () { + // Navigate back to previous page + Navigator.pop(context); + }, + icon: const Icon(Icons.arrow_back_ios), + ), + ), + body: ResultBodyWidget(result: result), + ); + } +} diff --git a/bmi_calculator/pubspec.yaml b/bmi_calculator/pubspec.yaml new file mode 100644 index 00000000..08f70b4b --- /dev/null +++ b/bmi_calculator/pubspec.yaml @@ -0,0 +1,26 @@ +name: bmi_calculator +description: A new Flutter module project. + +version: 1.0.0+1 + +environment: + sdk: ">=2.17.6 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + cupertino_icons: ^1.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +flutter: + uses-material-design: true + + module: + androidX: true + androidPackage: github.nisrulz.bmi_calculator + iosBundleIdentifier: github.nisrulz.bmiCalculator diff --git a/bmi_calculator/test/widget_test.dart b/bmi_calculator/test/widget_test.dart new file mode 100644 index 00000000..9c9ed412 --- /dev/null +++ b/bmi_calculator/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:bmi_calculator/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +}