goal : bmi calculator 앱 구현하기.
https://dribbble.com/shots/4585382-Simple-BMI-Calculator

'refactoring'을 사용하면서, 위 링크의 bmi 계산기 앱을 그대로 구현할 것이다.

main.dart 파일(테마지정)

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

첫페이지 구현

reusable_card.dart 파일(위젯추출)

화면의 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
  }
}

icon_content.dart 파일(위젯추출)

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

round_icon_button.dart 파일(버튼클래스의 컴포넌트지정, 위젯추출)

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

bottom_button.dart 파일(위젯추출)

맨밑의 빨간색 버튼은 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
  }
}

input_page.dart 파일(첫페이지화면)

추출해놓은 클래스들을 불러와서 첫페이지를 작성하자.

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

두번째페이지구현

result_page.dart(두번째페이지화면)

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

기능추가(bmi계산기능)

calculator_brain.dart 파일

'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.';
    }
  }
}

input_page.dart 파일의 buttombutton 클래스내의 function 변경

  • 변경전
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

constants.dart 파일(상수파일)

아래와 같이 상수들은 하나의 파일에 지정해놓았다.

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폴더 내에 다음과같이 지정했다.

profile
Flutter 학습 일기

0개의 댓글