BMI calculator example (#130)

This commit is contained in:
lutaii
2023-10-12 18:08:26 +02:00
committed by GitHub
parent f761e553e4
commit 36c8bed38c
19 changed files with 780 additions and 0 deletions

52
bmi_calculator/.gitignore vendored Normal file
View File

@ -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

10
bmi_calculator/.metadata Normal file
View File

@ -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

7
bmi_calculator/README.md Normal file
View File

@ -0,0 +1,7 @@
# bmi_calculator
A persons Body-Mass Index, or BMI, helps them check whether theyre 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 youre 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)

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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<CalculatorBody> createState() => _CalculatorBodyState();
}
class _CalculatorBodyState extends State<CalculatorBody> {
@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)),
),
);
},
),
],
),
),
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

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