goal : bmi calculator 앱 구현하기.
https://dribbble.com/shots/4585382-Simple-BMI-Calculator
'refactoring'을 사용하면서, 위 링크의 bmi 계산기 앱을 그대로 구현할 것이다.
MeterialApp 내에 'theme' 속성을 지정하여, 각 페이지 디자인의 일관성을 유지했다.
import 'package:flutter/material.dart';
import 'screens/input_page.dart';
void main() => runApp(BMICalculator());
class BMICalculator extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(
primaryColor: Color(0xFF0A0D22),
scaffoldBackgroundColor: Color(0xFF0A0D22)),
home: InputPage(),
); // MaterialApp
}
}
화면의 5개카드는 반복되는 코드구조를 가지므로, 위젯추출을 통해 'ReusableCard' 클래스로 지정하자.
import 'package:flutter/material.dart';
class ReusableCard extends StatelessWidget {
final Color colour;
final Widget cardchild;
final Function onPress;
ReusableCard({this.colour, this.cardchild, this.onPress});
Widget build(BuildContext context) {
return GestureDetector(
onTap: onPress,
child: Container(
margin: EdgeInsets.all(15),
child: cardchild,
decoration: BoxDecoration(
color: colour,
borderRadius: BorderRadius.circular(10),
), // BoxDecoration
), // Container
); // GestureDetector
}
}
1행의 카드 2개는 카드내의 내용이 '아이콘-텍스트'인 동일한 구조이므로, 위젝추출을 통해 'IconContent' 클래스로 지정하자.
import 'package:flutter/material.dart';
import '../constants.dart';
class IconContent extends StatelessWidget {
final IconData icon;
final String label;
IconContent(this.icon, this.label);
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: kIconsize,
),
SizedBox(height: 15),
Text(
label,
style: kLebletextStyle,
) // Text
],
); // Column
}
}
슬라이더의 이미지를 구체적으로 지정하고자, SliderThemeData를 통해 디자인했다.
Expanded(
child: ReusableCard(
colour: kActiveCardColour,
cardchild: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'HEIGHT',
style: kLebletextStyle,
), // Text
Row(
mainAxisAlignment: MainAxisAlignment.center,
textBaseline: TextBaseline.alphabetic,
crossAxisAlignment: CrossAxisAlignment.baseline,
children: [
Text(height.toString(), style: kNumberTextStyle),
Text(
'cm',
style: kLebletextStyle,
) // Text
],
), // Row
SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: Colors.white,
inactiveTrackColor: Color(0xFF8D8E98),
thumbColor: Color(0xFFEB1555),
thumbShape:
RoundSliderThumbShape(enabledThumbRadius: 15),
overlayShape:
RoundSliderOverlayShape(overlayRadius: 30),
overlayColor: Color(0x29EB1555)),
child: Slider(
min: 120,
max: 220,
value: height.toDouble(),
onChanged: (newValue) {
setState(() {
height = newValue.round();
});
},
), // Slider
) // SliderTheme
],
)), // Column, ReusableCard
), // Expanded
3행의 각 카드에는 동일한 모양의 버튼이 두개씩 존재하므로, 버튼을 구현한 코드를 위젯추출을 통해 'RoundIconButton' 클래스로 지정하자.
'FloatingActionButton'은 하나의 스크린당 하나밖에 사용하지 못한다.
https://api.flutter.dev/flutter/material/FloatingActionButton-class.html
따라서 'FloatingActionButton'의 컴포넌트인 'RawMaterialButton'을 사용했다.
※각 클래스는 여러컴포넌트로 구성되어있다. ctrl+click을 통해 확인할 수 있다.※
import 'package:flutter/material.dart';
class RoundIconButton extends StatelessWidget {
final IconData icon;
final Function onPressed;
RoundIconButton({ this.icon, this.onPressed});
Widget build(BuildContext context) {
return RawMaterialButton(
elevation: 6,
child: Icon(icon),
onPressed: onPressed,
fillColor: Color(0xFF4C4F52),
shape: CircleBorder(),
constraints: BoxConstraints.tightFor(width: 56, height: 56),
); // RawMaterialButton
}
}
맨밑의 빨간색 버튼은 2번째 화면에 나올 모습과 동일하므로 위젯추출을 통해 'BottomButton' 클래스로 지정하자.
import 'package:flutter/material.dart';
import '../constants.dart';
class BottomButton extends StatelessWidget {
final Function onTap;
final String buttonTitle;
BottomButton({this.onTap, this.buttonTitle});
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
child: Center(
child: Text(
buttonTitle,
style: kLargeButtonTextStyle,
), // Text
), // Center
margin: EdgeInsets.only(top: 10),
decoration: BoxDecoration(
color: kBottomContainerColour,
borderRadius: BorderRadius.circular(10)),
width: double.infinity,
height: kBottomheight,
), // Container
); // GestureDetector
}
}
추출해놓은 클래스들을 불러와서 첫페이지를 작성하자.
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import '../components/reusable_card.dart';
import '../components/icon_content.dart';
import '../constants.dart';
import 'result_page.dart';
import '../components/bottom_button.dart';
import '../components/round_icon_button.dart';
import 'package:bmi_calculator/calculator_brain.dart';
enum Gender { male, female }
class InputPage extends StatefulWidget {
_InputPageState createState() => _InputPageState();
}
class _InputPageState extends State<InputPage> {
Gender selectedGender;
int height = 183;
int weight = 60;
int age = 20;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Color(0xFF0A0D22),
title: Center(child: Text('BMI CALCULATOR')),
), // Appbar
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Row(
children: [
Expanded(
child: ReusableCard(
onPress: () {
setState(() {
selectedGender = Gender.male;
});
},
colour: selectedGender == Gender.male
? kActiveCardColour
: kInactiveCardColour,
cardchild: IconContent(FontAwesomeIcons.mars, 'MALE'),
), // ReusableCard
), // Expanded
Expanded(
child: ReusableCard(
onPress: () {
setState(() {
selectedGender = Gender.female;
});
},
colour: selectedGender == Gender.female
? kActiveCardColour
: kInactiveCardColour,
cardchild: IconContent(FontAwesomeIcons.venus, 'FEMALE')),
) // Expanded
],
), // Row
), // Expanded
Expanded(
child: ReusableCard(
colour: kActiveCardColour,
cardchild: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'HEIGHT',
style: kLebletextStyle,
), Text
Row(
mainAxisAlignment: MainAxisAlignment.center,
textBaseline: TextBaseline.alphabetic,
crossAxisAlignment: CrossAxisAlignment.baseline,
children: [
Text(height.toString(), style: kNumberTextStyle),
Text(
'cm',
style: kLebletextStyle,
) // Text
],
), // Row
SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: Colors.white,
inactiveTrackColor: Color(0xFF8D8E98),
thumbColor: Color(0xFFEB1555),
thumbShape:
RoundSliderThumbShape(enabledThumbRadius: 15),
overlayShape:
RoundSliderOverlayShape(overlayRadius: 30),
overlayColor: Color(0x29EB1555)),
child: Slider(
min: 120,
max: 220,
value: height.toDouble(),
onChanged: (newValue) {
setState(() {
height = newValue.round();
});
},
), // Slider
) // SliderTheme
],
)), // Column, ReusableCard
), // Expanded
Expanded(
child: Row(
children: [
Expanded(
child: ReusableCard(
colour: kActiveCardColour,
cardchild: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'WEIGHT',
style: kLebletextStyle,
), // Text
Text(
weight.toString(),
style: kNumberTextStyle,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
RoundIconButton(
icon: FontAwesomeIcons.minus,
onPressed: () {
setState(() {
weight--;
});
},
), // RoundIconButton
SizedBox(width: 10),
RoundIconButton(
icon: FontAwesomeIcons.plus,
onPressed: () {
setState(() {
weight++;
});
},
), // RoundIconButton
],
) // Row
],
)), // Column, ReusableCard
), // Expanded
Expanded(
child: ReusableCard(
colour: kActiveCardColour,
cardchild: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'AGE',
style: kLebletextStyle,
), // Text
Text(
age.toString(),
style: kNumberTextStyle,
), // Text
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
RoundIconButton(
icon: FontAwesomeIcons.minus,
onPressed: () {
setState(() {
age--;
});
},
), // RoundIconButton
SizedBox(width: 10),
RoundIconButton(
icon: FontAwesomeIcons.plus,
onPressed: () {
setState(() {
age++;
});
},
), // RoundIconButton
],
) // Row
],
)), // Column, ReusableCard
) // Expanded
],
), // Row
), // Expanded
BottomButton(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ResultPage())); // ResultPage, MaterialPageRoute
},
buttonTitle: 'CALCULATE YOUR BMI',
) // BottomButton
],
), // Column
); // Scaffold
}
}
import 'package:bmi_calculator/components/reusable_card.dart';
import 'package:flutter/material.dart';
import '../constants.dart';
import '../components/bottom_button.dart';
class ResultPage extends StatelessWidget {
final String bmiResult;
final String resultText;
final String interpretation;
ResultPage(
{ this.bmiResult,
this.resultText,
this.interpretation});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Color(0xFF0A0D22),
title: Text('BMI CALCULATOR', style: kLebletextStyle),
), // AppBar
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Container(
padding: EdgeInsets.all(13.0),
alignment: Alignment.bottomLeft,
child: Text(
'Your Result',
style: kTitleTextStyle,
), // Text
), // Container
), // Expanded
Expanded(
flex: 5,
child: ReusableCard(
colour: kActiveCardColour,
cardchild: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
resultText.toUpperCase(),
style: kResultTextStyle,
), // Text
Text(
bmiResult,
style: kBMITextStyle,
), // Text
Text(
interpretation,
textAlign: TextAlign.center,
style: kBodyTextStyle,
) // Text
],
), Column
), // ReusableCard
), // Expanded
Expanded(
child: BottomButton(
buttonTitle: 'RE-CACULATEYOURBMI',
onTap: () {
Navigator.pop(context);
},
), // BottomButton
) // Expanded
],
), // Column
); // Scaffold
}
}
'CalculatorBrain' 클래스를 생성하고 속성과 메소드, 생성자를 설정하자.
import 'dart:math'; // 제곱값을 구하는 pow()메소드를 사용하기 위함
class CalculatorBrain {
final int height;
final int weight;
double _bmi;
CalculatorBrain({this.height, this.weight});
String calculateBMI() {
_bmi = weight / pow(height / 100, 2);
return _bmi.toStringAsFixed(1);
}
String getResult() {
if (_bmi >= 25) {
return 'Overweight';
} else if (_bmi >= 18.5) {
return 'Normal';
} else {
return 'Underweight';
}
}
String getInterpretation() {
if (_bmi >= 25) {
return 'You have a higher than normal body weight. Try to exercise more.';
} else if (_bmi >= 18.5) {
return 'You have a normal body weight. Good job!';
} else {
return 'You have a lower than normal body weight. You can eat a bit more.';
}
}
}
BottomButton(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ResultPage())); // ResultPage, MaterialPageRoute
},
buttonTitle: 'CALCULATE YOUR BMI',
) // BottomButton
BottomButton(
onTap: () {
CalculatorBrain calculate =
CalculatorBrain(height: height, weight: weight);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ResultPage(
bmiResult: calculate.calculateBMI(),
resultText: calculate.getResult(),
interpretation: calculate.getInterpretation(),
))); // ResultPage, MaterialPageRoute
},
buttonTitle: 'CALCULATE YOUR BMI',
) // BottomButton
아래와 같이 상수들은 하나의 파일에 지정해놓았다.
import 'package:flutter/material.dart';
const kLebletextStyle = TextStyle(fontSize: 18, color: Color(0xFFF8D8E98));
const kLargeButtonTextStyle =
TextStyle(fontSize: 25, fontWeight: FontWeight.bold);
const double kIconsize = 80;
const double kBottomheight = 80;
const kActiveCardColour = Color(0xFF1D1F33);
const kInactiveCardColour = Color(0xFF111328);
const kBottomContainerColour = Color(0xFFEB1555);
const kNumberTextStyle = TextStyle(fontSize: 50, fontWeight: FontWeight.w900);
const kTitleTextStyle = TextStyle(fontSize: 50, fontWeight: FontWeight.bold);
const kResultTextStyle = TextStyle(
color: Color(0xFF24D876), fontSize: 22, fontWeight: FontWeight.bold);
const kBMITextStyle = TextStyle(fontSize: 100, fontWeight: FontWeight.bold);
const kBodyTextStyle = TextStyle(fontSize: 22);
lib
폴더 내에 다음과같이 지정했다.