๐ Today I Learned 20ํ์ฐจ(1) - Flutter BMI ๊ณ์ฐ๊ธฐ ์ฑ ๋ง๋ค๊ธฐ ๐ก
์ค๋์ Flutter๋ก ์ค์ BMI ๊ณ์ฐ๊ธฐ ์ฑ์ ๋ง๋ค์ด๋ณด์์ด์! ํ ๋ง ์ค์ ๋ถํฐ ํ์ด์ง ๋ผ์ฐํ , ์์ ฏ ์ฌ์ฌ์ฉ๊น์ง ๋ค์ํ ๊ฐ๋ ์ ์ค์ตํ๋ฉด์ ์ตํ๋ต๋๋ค. ํนํ Material3 ๋์์ธ ์์คํ ๊ณผ ๋คํฌ๋ชจ๋ ์ง์, Navigator๋ฅผ ์ด์ฉํ ๋ฐ์ดํฐ ์ ๋ฌ ๋ฐฉ๋ฒ์ ๋ฐฐ์ ์ด์.
๐ฏ ํ์ต ๋ชฉํ
- Material3 ํ ๋ง ์์คํ ์ดํดํ๊ณ ๋ผ์ดํธ/๋คํฌ ํ ๋ง ๊ตฌํํ๊ธฐ
- Navigator๋ฅผ ์ฌ์ฉํ ํ์ด์ง ๋ผ์ฐํ ๊ณผ ๋ฐ์ดํฐ ์ ๋ฌ ๋ฐฉ๋ฒ ์ตํ๊ธฐ
- RichText ์์ ฏ์ผ๋ก ๋ค์ํ ํ ์คํธ ์คํ์ผ ์กฐํฉํ๊ธฐ
- ์ค์ ํ๋ก์ ํธ๋ฅผ ํตํด ์์ ฏ ์ฌ์ฌ์ฉ๊ณผ ์ํ ๊ด๋ฆฌ ์ฐ์ตํ๊ธฐ
๐ ์ฃผ์ ๋ด์ฉ
โญ 1. Flutter ํ ๋ง ์ค์ (ThemeData)
Flutter์์ ์ฑ ์ ์ฒด์ ๋์์ธ์ ์ผ๊ด๋๊ฒ ๊ด๋ฆฌํ๋ ค๋ฉด ThemeData๋ฅผ ์ฌ์ฉํด์. MaterialApp์ theme๊ณผ darkTheme ์์ฑ์ผ๋ก ๋ผ์ดํธ/๋คํฌ ํ
๋ง๋ฅผ ๊ฐ๊ฐ ์ ์ํ ์ ์๋ต๋๋ค.
๐ ThemeData ์ฃผ์ ์์ฑ
| ์์ฑ | ์ค๋ช | ์์ |
|---|---|---|
| useMaterial3 | Material3 ๋์์ธ ์์คํ ์ฌ์ฉ ์ฌ๋ถ | true / false |
| colorScheme | ์ฑ์ ์์ ์ฒด๊ณ ์ ์ | ColorScheme.fromSeed() |
| textTheme | ํ ์คํธ ์คํ์ผ ์ ์ | TextTheme(...) |
| brightness | ๋ฐ๊ธฐ ๋ชจ๋ (๋ผ์ดํธ/๋คํฌ) | Brightness.light / Brightness.dark |
๐ก Tip:
ColorScheme.fromSeed()๋ฅผ ์ฌ์ฉํ๋ฉด ํ๋์seedColor๋ฅผ ๊ธฐ์ค์ผ๋ก ์ฑ ์ ์ฒด์ ์์ ํ๋ ํธ๊ฐ ์๋์ผ๋ก ์์ฑ๋ผ์!
// ํ
๋ง ์ค์ ์์
ThemeData(
// Material3 ๋์์ธ ์์คํ
์ฌ์ฉ (Material2์ ์ฐจ์ด: ๋ฒํผ, ์นด๋ ๋ฑ์ ์คํ์ผ์ด ๋ ํ๋์ )
useMaterial3: true,
// ColorScheme.fromSeed: seedColor๋ฅผ ๊ธฐ์ค์ผ๋ก ์๋์ผ๋ก ์์ ํ๋ ํธ ์์ฑ
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.purple, // ๊ธฐ๋ณธ ์์
brightness: Brightness.dark, // ๋คํฌ๋ชจ๋ ํ์ฑํ
),
// ์ฑ ๋ด์์ ์ฌ์ฉํ ํ
์คํธ ์คํ์ผ ์ ์
// displayLarge: ํฐ ์ ๋ชฉ, titleLarge: ์น์
์ ๋ชฉ, bodyMedium: ๋ณธ๋ฌธ ๋ฑ
textTheme: TextTheme(
displayLarge: const TextStyle(
fontSize: 72,
fontWeight: FontWeight.bold,
),
titleLarge: GoogleFonts.oswald( // Google Fonts ์ฌ์ฉ ๊ฐ๋ฅ
fontSize: 30,
fontStyle: FontStyle.italic,
),
bodyMedium: GoogleFonts.merriweather(),
displaySmall: GoogleFonts.pacifico(),
),
)
โก 2. Navigator๋ฅผ ์ด์ฉํ ํ์ด์ง ๋ผ์ฐํ
Flutter์์ ํ๋ฉด ์ ํ์ Navigator๋ฅผ ์ฌ์ฉํด์. Navigator๋ ์คํ ๊ตฌ์กฐ๋ก ํ์ด์ง๋ฅผ ๊ด๋ฆฌํ๋ฉฐ, push๋ก ์ ํ์ด์ง๋ฅผ ์ถ๊ฐํ๊ณ pop์ผ๋ก ์ด์ ํ์ด์ง๋ก ๋์๊ฐ์.
๐ ์ฐธ๊ณ : Flutter ๊ณต์ ๋ฌธ์ - Navigation
๐ Navigator ์ฃผ์ ๋ฉ์๋
Navigator (Stack ๊ตฌ์กฐ)
โ
โโ push() โ ์ ํ์ด์ง ์ถ๊ฐ
โโ pop() โ ํ์ฌ ํ์ด์ง ์ ๊ฑฐ (๋ค๋ก๊ฐ๊ธฐ)
โโ pushReplacement() โ ํ์ฌ ํ์ด์ง๋ฅผ ์ ํ์ด์ง๋ก ๊ต์ฒด
๐ ๋ฐฉ๋ฒ 1: RouteSettings๋ฅผ ์ด์ฉํ ๋ฐ์ดํฐ ์ ๋ฌ
// ๋ฐ์ดํฐ๋ฅผ ๋๊ธฐ๋ฉฐ ํ์ด์ง ์ด๋
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ResultPage();
},
settings: RouteSettings(
arguments: result // ๋๊ธธ ๋ฐ์ดํฐ
)
),
);
// ์๋ก์ด ํ์ด์ง์์ ๋ฐ์ดํฐ ๋ฐ๊ธฐ
final args = ModalRoute.of(context)!.settings.arguments;
๐ฏ ๋ฐฉ๋ฒ 2: ์์ฑ์๋ฅผ ์ด์ฉํ ๋ฐ์ดํฐ ์ ๋ฌ (์ถ์ฒ!)
์ด ๋ฐฉ๋ฒ์ด ๋ ํ์ ์์ ํ๊ณ ์ดํดํ๊ธฐ ์ฌ์์!
// ํ์ด์ง ์ด๋ํ๋ฉด์ ์์ฑ์๋ก ๋ฐ์ดํฐ ์ ๋ฌ
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ResultPage(bmi); // ์์ฑ์์ ์ง์ ์ ๋ฌ
},
),
);
// ResultPage ํด๋์ค์์ ์์ฑ์๋ก ๋ฐ๊ธฐ
class ResultPage extends StatelessWidget {
final double bmi;
ResultPage(this.bmi); // ์์ฑ์์์ ๋ฐ์
// ...
}
โฌ ๏ธ ๋๋์๊ฐ๋ฉด์ ๋ฐ์ดํฐ ์ ๋ฌํ๊ธฐ
ํ์ด์ง B์์ ํ์ด์ง A๋ก ๋์๊ฐ๋ฉด์ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ ์๋ ์์ด์!
// B ํ์ด์ง์์: pop ํธ์ถ ์ ๋ฐ์ดํฐ๋ฅผ ํจ๊ป ๋ฐํ
ElevatedButton(
onPressed: () {
Navigator.pop(context, 'Nope.'); // ๋์๊ฐ๋ฉด์ ๋ฐ์ดํฐ ์ ๋ฌ
},
child: const Text('Nope.'),
)
// A ํ์ด์ง์์: await๋ก ๊ฒฐ๊ณผ ๋ฐ๊ธฐ
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SelectionScreen()),
);
// โ ๏ธ ์ค์: ์๋๋ก์ด๋ ๋ค๋ก๊ฐ๊ธฐ ๋ฒํผ์ด๋ ์ ์ค์ฒ๋ก ๋ค๋ก๊ฐ๋ฉด null ๋ฐํ!
if (result != null) {
// ๋ฐ์ ๋ฐ์ดํฐ ์ฒ๋ฆฌ
}
๐จ 3. RichText - ๋ค์ํ ํ ์คํธ ์คํ์ผ ์กฐํฉ
ํ๋์ ํ ์คํธ ์์ ฏ ์์์ ๋ถ๋ถ์ ์ผ๋ก ๋ค๋ฅธ ์คํ์ผ์ ์ ์ฉํ๊ณ ์ถ์ ๋ RichText๋ฅผ ์ฌ์ฉํด์. ์๋ฅผ ๋ค์ด, โResult: NORMAL (BMI 18.5 - 25)โ์ฒ๋ผ ๊ฐ์ด๋ฐ ๋ถ๋ถ๋ง ์์์ด๋ ๊ตต๊ธฐ๋ฅผ ๋ค๋ฅด๊ฒ ํํํ ์ ์์ด์.
// RichText ์์: ํ
์คํธ ์ผ๋ถ๋ง ๋ค๋ฅธ ์คํ์ผ ์ ์ฉ
RichText(
text: TextSpan(
// ๊ธฐ๋ณธ ์คํ์ผ ์ค์
style: TextStyle(
fontSize: 18,
color: Theme.of(context).textTheme.bodyLarge?.color,
),
children: [
TextSpan(text: 'Result: '), // ๊ธฐ๋ณธ ์คํ์ผ
TextSpan(
text: 'NORMAL', // ์ด ๋ถ๋ถ๋ง ๋ค๋ฅธ ์คํ์ผ
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).highlightColor, // ๊ฐ์กฐ ์์
),
),
TextSpan(text: ' (BMI 18.5 - 25)'), // ๋ค์ ๊ธฐ๋ณธ ์คํ์ผ
],
),
)
๐ก ์ธ์ ์ฌ์ฉํ๋์?
- ํ ์ค์ ํ ์คํธ์์ ์ผ๋ถ๋ง ์์, ๊ตต๊ธฐ, ํฌ๊ธฐ๋ฅผ ๋ค๋ฅด๊ฒ ํ๊ณ ์ถ์ ๋
- ์ฌ๋ฌ ๊ฐ์ Text ์์ ฏ์ ๋์ดํ๋ ๊ฒ๋ณด๋ค ๋ ๊น๋ํ๊ฒ ์ฒ๋ฆฌ ๊ฐ๋ฅ
๐ป ์ค์ ํ๋ก์ ํธ: BMI ๊ณ์ฐ๊ธฐ ์ฑ
๋ฐฐ์ด ๋ด์ฉ์ ์ข ํฉํด์ BMI ๊ณ์ฐ๊ธฐ ์ฑ์ ๋ง๋ค์ด๋ดค์ด์!
๐๏ธ ํ๋ก์ ํธ ๊ตฌ์กฐ
lib/
โโโ main.dart // ์ฑ ์ง์
์ & ํ
๋ง ์ค์
โโโ theme.dart // lightTheme, darkTheme ์ ์
โโโ pages/
โโโ home/
โ โโโ home_pages.dart // ํ ํ๋ฉด (์
๋ ฅ ํ์ด์ง)
โ โโโ widgets/
โ โโโ gender_box.dart // ์ฑ๋ณ ์ ํ ์์ ฏ
โ โโโ slider_box.dart // ํค/๋ชธ๋ฌด๊ฒ ์
๋ ฅ ์ฌ๋ผ์ด๋
โโโ results/
โโโ results_page.dart // ๊ฒฐ๊ณผ ํ๋ฉด
โโโ widgets/
โโโ result_guage.dart // BMI ๊ฒ์ด์ง (CircularProgressIndicator)
โโโ result_text.dart // ๊ฒฐ๊ณผ ํ
์คํธ (RichText ์ฌ์ฉ)
๐ฑ 1. ๋ฉ์ธ ์ฑ & ํ ๋ง ์ ์ฉ
// main.dart - ์ฑ ์ง์
์
import 'package:flutter/material.dart';
import 'package:flutter_bmi_app/pages/home/home_pages.dart';
import 'package:flutter_bmi_app/theme.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
// ์์คํ
์ค์ ์ ๋ฐ๋ผ ์๋์ผ๋ก ํ
๋ง ์ ํ
themeMode: ThemeMode.system,
theme: lightTheme, // ๋ผ์ดํธ ํ
๋ง
darkTheme: darkTheme, // ๋คํฌ ํ
๋ง
home: HomePages(),
);
}
}
๐จ 2. ๋ผ์ดํธ/๋คํฌ ํ ๋ง ์ ์
// theme.dart - ๋ผ์ดํธ ํ
๋ง ์ ์
import 'package:flutter/material.dart';
final lightTheme = ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.pinkAccent, // ํํฌ ๊ธฐ๋ฐ ์์ ํ๋ ํธ
brightness: Brightness.light,
),
dividerColor: Colors.black38, // ๊ตฌ๋ถ์ ์์
highlightColor: Colors.pinkAccent, // ๊ฐ์กฐ ์์
// ์ฌ๋ผ์ด๋ ์ปค์คํฐ๋ง์ด์ง
sliderTheme: SliderThemeData(
thumbColor: Colors.pinkAccent, // ์ฌ๋ผ์ด๋ ์์ก์ด
activeTrackColor: Colors.black38, // ํ์ฑ ํธ๋
inactiveTrackColor: Colors.black38, // ๋นํ์ฑ ํธ๋
trackHeight: 1,
),
// ElevatedButton ์ปค์คํฐ๋ง์ด์ง
elevatedButtonTheme: ElevatedButtonThemeData(
style: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder()), // ๊ฐ์ง ๋ฒํผ
backgroundColor: WidgetStatePropertyAll(Colors.pinkAccent),
foregroundColor: WidgetStatePropertyAll(Colors.white),
),
),
// OutlinedButton ์ปค์คํฐ๋ง์ด์ง
outlinedButtonTheme: OutlinedButtonThemeData(
style: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder()),
side: WidgetStatePropertyAll(BorderSide(color: Colors.black38)),
foregroundColor: WidgetStatePropertyAll(Colors.black),
),
),
);
// theme.dart - ๋คํฌ ํ
๋ง ์ ์
final darkTheme = ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.pinkAccent,
brightness: Brightness.dark, // ๋คํฌ๋ชจ๋
),
dividerColor: Colors.white38, // ๋คํฌ๋ชจ๋์ฉ ๊ตฌ๋ถ์
highlightColor: Colors.pinkAccent,
sliderTheme: SliderThemeData(
thumbColor: Colors.pinkAccent,
activeTrackColor: Colors.white30, // ๋คํฌ๋ชจ๋์ฉ ํธ๋ ์์
inactiveTrackColor: Colors.white30,
trackHeight: 1,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder()),
backgroundColor: WidgetStatePropertyAll(Colors.pinkAccent),
foregroundColor: WidgetStatePropertyAll(Colors.white),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder()),
side: WidgetStatePropertyAll(BorderSide(color: Colors.white30)),
foregroundColor: WidgetStatePropertyAll(Colors.white),
),
),
);
๐ 3. ํ ํ์ด์ง - ๋ฐ์ดํฐ ์ ๋ ฅ ํ๋ฉด
// home_pages.dart - StatefulWidget์ผ๋ก ์ํ ๊ด๋ฆฌ
import 'package:flutter/material.dart';
import 'package:flutter_bmi_app/pages/home/widgets/gender_box.dart';
import 'package:flutter_bmi_app/pages/home/widgets/slider_box.dart';
import 'package:flutter_bmi_app/pages/results/results_page.dart';
class HomePages extends StatefulWidget {
@override
State<HomePages> createState() => _HomePagesState();
}
class _HomePagesState extends State<HomePages> {
// ์ํ ๋ณ์๋ค
bool isMale = true;
double height = 170;
double weight = 70;
// ์ฑ๋ณ ๋ณ๊ฒฝ ์ฝ๋ฐฑ
void onGenderChanged(bool male) {
setState(() {
isMale = male;
});
}
// ํค ๋ณ๊ฒฝ ์ฝ๋ฐฑ
void onHeightChanged(double newHeight) {
setState(() {
height = newHeight;
});
}
// ๋ชธ๋ฌด๊ฒ ๋ณ๊ฒฝ ์ฝ๋ฐฑ
void onWeightChanged(double newWeight) {
setState(() {
weight = newWeight;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('BMI CALCUALATOR')),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 50),
child: Column(
children: [
// ์ฑ๋ณ ์ ํ ์์ ฏ
GenderBox(isMale, onGenderChanged),
Spacer(), // ์๋ ๊ฐ๊ฒฉ ์ฑ์ฐ๊ธฐ
// ํค ์
๋ ฅ ์ฌ๋ผ์ด๋
SliderBox(
label: 'HEIGHT',
value: height,
unit: 'cm',
onChanged: onHeightChanged,
),
Spacer(),
// ๋ชธ๋ฌด๊ฒ ์
๋ ฅ ์ฌ๋ผ์ด๋
SliderBox(
label: 'WEIGHT',
value: weight,
unit: 'kg',
onChanged: onWeightChanged,
),
Spacer(),
// BMI ๊ณ์ฐ ๋ฒํผ
SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: () {
// BMI ๊ณ์ฐ: ์ฒด์ค(kg) / (์ ์ฅ(m) * ์ ์ฅ(m))
final meterHeight = height / 100;
final bmi = weight / (meterHeight * meterHeight);
// ๊ฒฐ๊ณผ ํ์ด์ง๋ก ์ด๋ (์์ฑ์๋ก BMI ๊ฐ ์ ๋ฌ)
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ResultsPage(bmi);
},
),
);
},
child: Text('CALCULATOR'),
),
),
SizedBox(height: 10),
],
),
),
);
}
}
๐ค 4. ์ฑ๋ณ ์ ํ ์์ ฏ (GenderBox)
// gender_box.dart - ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ฑ๋ณ ์ ํ ์์ ฏ
import 'package:flutter/material.dart';
class GenderBox extends StatelessWidget {
bool isMale;
void Function(bool isMale) onChanged;
GenderBox(this.isMale, this.onChanged);
@override
Widget build(BuildContext context) {
return Row(
children: [
gender(context, Icons.male, 'MALE', isMale, true),
SizedBox(width: 8),
gender(context, Icons.female, 'FEMALE', !isMale, false),
],
);
}
Widget gender(
BuildContext context,
IconData icon,
String text,
bool selected,
bool isMaleBox,
) {
return Expanded(
child: GestureDetector(
onTap: () {
onChanged(isMaleBox);
},
child: Container(
height: 150,
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).dividerColor),
),
// Stack์ผ๋ก ์์ด์ฝ๊ณผ ํ
์คํธ๋ฅผ ๊ฒน์ณ์ ๋ฐฐ์น
child: Stack(
children: [
// ๋ฐฐ๊ฒฝ ์์ด์ฝ (๋ฐํฌ๋ช
)
Positioned(
top: 10,
left: 10,
child: Opacity(opacity: 0.3, child: Icon(icon, size: 80)),
),
// ํ
์คํธ (์ ํ ์ฌ๋ถ์ ๋ฐ๋ผ ํฌ๋ช
๋ ๋ณ๊ฒฝ)
Positioned(
right: 20,
bottom: 20,
child: Opacity(
opacity: selected ? 1 : 0.3, // ์ ํ๋๋ฉด ๋ถํฌ๋ช
child: Text(
text,
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
),
),
],
),
),
),
);
}
}
๐ 5. ์ฌ๋ผ์ด๋ ์์ ฏ (SliderBox)
// slider_box.dart - ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ฌ๋ผ์ด๋ ์์ ฏ
import 'package:flutter/material.dart';
class SliderBox extends StatelessWidget {
SliderBox({
required this.label,
required this.value,
required this.unit,
required this.onChanged,
});
String label; // ๋ผ๋ฒจ (HEIGHT, WEIGHT)
double value; // ํ์ฌ ๊ฐ
String unit; // ๋จ์ (cm, kg)
void Function(double newValue) onChanged; // ๊ฐ ๋ณ๊ฒฝ ์ฝ๋ฐฑ
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(label, style: TextStyle(fontSize: 20)),
Spacer(),
// ์์์ ์์ด ์ ์๋ก ํ์
Text(
value.toStringAsFixed(0),
style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
),
Text(unit, style: TextStyle(fontSize: 20)),
],
),
// ์ฌ๋ผ์ด๋ (1~300 ๋ฒ์)
Slider(
value: value,
onChanged: onChanged,
min: 1,
max: 300,
),
],
);
}
}
๐ 6. ๊ฒฐ๊ณผ ํ์ด์ง (ResultsPage)
// results_page.dart - BMI ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ฌ์ฃผ๋ ํ์ด์ง
import 'package:flutter/material.dart';
import 'package:flutter_bmi_app/pages/results/widgets/result_guage.dart';
import 'package:flutter_bmi_app/pages/results/widgets/result_text.dart';
class ResultsPage extends StatelessWidget {
double bmi;
// ์์ฑ์๋ก BMI ๊ฐ ๋ฐ๊ธฐ
ResultsPage(this.bmi);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('BMI CALCUALATOR')),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
Spacer(),
// ์ํ ๊ฒ์ด์ง๋ก BMI ํ์
ResultGuage(bmi),
SizedBox(height: 50),
// RichText๋ก ๊ฒฐ๊ณผ ํ
์คํธ ํ์
ResultText(bmi: bmi),
Spacer(),
// ์ฌ๊ณ์ฐ ๋ฒํผ
SizedBox(
width: double.infinity,
height: 56,
child: OutlinedButton(
onPressed: () {
Navigator.pop(context); // ์ด์ ํ๋ฉด์ผ๋ก ๋์๊ฐ๊ธฐ
},
child: Text('RECALCULATE'),
),
),
SizedBox(height: 50),
],
),
),
);
}
}
๐ฏ 7. BMI ๊ฒ์ด์ง ์์ ฏ (ResultGuage)
// result_guage.dart - ์ํ ์งํ๋ฅ ํ์๊ธฐ๋ก BMI ์๊ฐํ
import 'dart:math';
import 'package:flutter/material.dart';
class ResultGuage extends StatelessWidget {
double bmi;
ResultGuage(this.bmi);
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
children: [
// ๋ฐฐ๊ฒฝ ์ํ ๊ฒ์ด์ง (ํ์, 100%)
SizedBox.square(
dimension: 250,
child: CircularProgressIndicator(
value: 1, // ์ ์ฒด ์
color: Theme.of(context).dividerColor,
),
),
// BMI ํ์ ์ํ ๊ฒ์ด์ง (ํํฌ, BMI/35)
SizedBox.square(
dimension: 250,
child: CircularProgressIndicator(
value: min(bmi / 35, 1), // BMI 35๋ฅผ ์ต๋๊ฐ์ผ๋ก (35 ์ด์์ 100%)
color: Theme.of(context).highlightColor,
),
),
// ์ค์์ BMI ์ซ์ ํ์
Text(
bmi.toStringAsFixed(1), // ์์์ ์ฒซ์งธ์๋ฆฌ๊น์ง
style: TextStyle(fontSize: 20)
),
],
);
}
}
๐ 8. ๊ฒฐ๊ณผ ํ ์คํธ ์์ ฏ (ResultText) - RichText ํ์ฉ
// result_text.dart - RichText๋ก BMI ๊ฒฐ๊ณผ๋ฅผ ๋ถ๋ถ ๊ฐ์กฐํ์ฌ ํ์
import 'package:flutter/material.dart';
class ResultText extends StatelessWidget {
final double bmi;
ResultText({required this.bmi});
// BMI ๊ฐ์ ๋ฐ๋ฅธ ๊ฒฐ๊ณผ ํ
์คํธ ๋ฐํ
String getResultText() {
if (bmi >= 35) {
return 'EX OBESE';
} else if (bmi >= 30) {
return 'OBESE';
} else if (bmi >= 25) {
return 'OVERWEIGHT';
} else if (bmi >= 18.5) {
return 'NORMAL';
} else {
return 'UNDERWEIGHT';
}
}
// BMI ๋ฒ์ ํ
์คํธ ๋ฐํ
String getResultRange() {
if (bmi >= 35) {
return '>= 35';
} else if (bmi >= 30) {
return '30 - 35';
} else if (bmi >= 25) {
return '25 - 30';
} else if (bmi >= 18.5) {
return '18.5 - 25';
} else {
return '< 18.5';
}
}
@override
Widget build(BuildContext context) {
// RichText ์ฌ์ฉ: ํ
์คํธ ์ผ๋ถ๋ง ๋ค๋ฅธ ์คํ์ผ ์ ์ฉ
return RichText(
text: TextSpan(
style: TextStyle(
fontSize: 18,
color: Theme.of(context).textTheme.bodyLarge?.color,
),
children: [
TextSpan(text: 'Result: '),
// ๊ฒฐ๊ณผ ํ
์คํธ๋ง ๊ฐ์กฐ (๊ตต๊ฒ + ๊ฐ์กฐ ์์)
TextSpan(
text: getResultText(),
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).highlightColor,
),
),
TextSpan(text: ' (BMI ${getResultRange()})'),
],
),
);
}
}
๐ก ํต์ฌ ๊ฐ๋ ์ ๋ฆฌ
๐จ ํ ๋ง ์์คํ
ThemeData
โโ ColorScheme.fromSeed() โ seedColor ๊ธฐ๋ฐ ์๋ ์์ ํ๋ ํธ
โโ Brightness โ light / dark ๋ชจ๋
โโ SliderTheme โ ์ฌ๋ผ์ด๋ ์ปค์คํฐ๋ง์ด์ง
โโ ElevatedButtonTheme โ ๋ฒํผ ์คํ์ผ ์ ์ญ ์ค์
โโ OutlinedButtonTheme โ ์์๋ผ์ธ ๋ฒํผ ์คํ์ผ
๐งญ Navigator ํจํด
A ํ์ด์ง โ Navigator.push() โ B ํ์ด์ง
โ Navigator.pop() โ
์ ๋ฌ ๋ฐฉ๋ฒ:
1. RouteSettings.arguments (๋ฒ์ฉ)
2. ์์ฑ์ ํ๋ผ๋ฏธํฐ (ํ์
์์ , ์ถ์ฒ)
๐ฆ ์์ ฏ ์ฌ์ฌ์ฉ
- SliderBox: ๋ผ๋ฒจ, ๊ฐ, ๋จ์, ์ฝ๋ฐฑ์ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ์ ์ฌ์ฌ์ฉ
- GenderBox: ์ ํ ์ํ์ ์ฝ๋ฐฑ์ ๋ฐ์ ์ฌ์ฌ์ฉ
- ์ปค์คํ ์์ ฏ์ ์ฅ์ : ์ฝ๋ ์ค๋ณต ์ ๊ฑฐ, ์ ์ง๋ณด์ ์ฉ์ด
๐ข ์ํ ๊ด๋ฆฌ
- StatefulWidget: ๋ณ๊ฒฝ ๊ฐ๋ฅํ ์ํ (ํค, ๋ชธ๋ฌด๊ฒ, ์ฑ๋ณ)
- setState(): ์ํ ๋ณ๊ฒฝ ์ UI ์๋ ์ ๋ฐ์ดํธ
- ์ฝ๋ฐฑ ํจ์: ์์ ์์ ฏ์์ ๋ถ๋ชจ ์์ ฏ์ ์ํ ๋ณ๊ฒฝ
โ ์ค๋ ๋ฐฐ์ด ๊ฒ
- ๐จ ํ ๋ง ์์คํ : Material3์ ColorScheme.fromSeed()๋ฅผ ํ์ฉํ ์ผ๊ด๋ ๋์์ธ ๊ตฌํ
- ๐งญ ํ์ด์ง ๋ผ์ฐํ : Navigator.push/pop์ผ๋ก ํ์ด์ง ์ด๋๊ณผ ๋ฐ์ดํฐ ์ ๋ฌ ๋ฐฉ๋ฒ ์ตํ
- โจ RichText: ํ๋์ ํ ์คํธ์์ ๋ถ๋ถ ๊ฐ์กฐ ์คํ์ผ ์ ์ฉ ๋ฐฉ๋ฒ
- ๐ฆ ์์ ฏ ์ฌ์ฌ์ฉ: ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ปค์คํ ์์ ฏ ์ค๊ณ ๋ฐฉ๋ฒ (GenderBox, SliderBox)
- ๐ข ์ํ ๊ด๋ฆฌ: StatefulWidget๊ณผ setState()๋ฅผ ์ด์ฉํ ์ํ ๊ด๋ฆฌ
- ๐๏ธ ํ๋ก์ ํธ ๊ตฌ์กฐ: pages์ widgets ํด๋๋ก ์ฒด๊ณ์ ์ธ ์ฝ๋ ๊ตฌ์กฐํ
๐ ๋ค์ ํ์ต ๋ชฉํ
- ์ํ ๊ด๋ฆฌ ์ฌํ: Provider ๋๋ Riverpod๋ฅผ ํ์ฉํ ์ ์ญ ์ํ ๊ด๋ฆฌ
- ์ ๋๋ฉ์ด์ : Flutter์ Animation๊ณผ AnimatedContainer ํ์ต
- ๋ฐ์ดํฐ ์ ์ฅ: SharedPreferences๋ก ์ฌ์ฉ์ ๋ฐ์ดํฐ ๋ก์ปฌ ์ ์ฅ
- API ์ฐ๋: http ํจํค์ง๋ฅผ ํ์ฉํ REST API ํต์
- ์ฑ ์์ฑ๋: ์ ๋ ฅ ์ ํจ์ฑ ๊ฒ์ฌ, ์๋ฌ ์ฒ๋ฆฌ, ์ฌ์ฉ์ ๊ฒฝํ ๊ฐ์
๋๊ธ๋จ๊ธฐ๊ธฐ