๐Ÿ“š Today I Learned 20ํšŒ์ฐจ(1) - Flutter BMI ๊ณ„์‚ฐ๊ธฐ ์•ฑ ๋งŒ๋“ค๊ธฐ ๐Ÿ’ก

9 ๋ถ„ ์†Œ์š”

์˜ค๋Š˜์€ 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 ํด๋”๋กœ ์ฒด๊ณ„์ ์ธ ์ฝ”๋“œ ๊ตฌ์กฐํ™”

๐Ÿš€ ๋‹ค์Œ ํ•™์Šต ๋ชฉํ‘œ

  1. ์ƒํƒœ ๊ด€๋ฆฌ ์‹ฌํ™”: Provider ๋˜๋Š” Riverpod๋ฅผ ํ™œ์šฉํ•œ ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ
  2. ์• ๋‹ˆ๋ฉ”์ด์…˜: Flutter์˜ Animation๊ณผ AnimatedContainer ํ•™์Šต
  3. ๋ฐ์ดํ„ฐ ์ €์žฅ: SharedPreferences๋กœ ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ๋กœ์ปฌ ์ €์žฅ
  4. API ์—ฐ๋™: http ํŒจํ‚ค์ง€๋ฅผ ํ™œ์šฉํ•œ REST API ํ†ต์‹ 
  5. ์•ฑ ์™„์„ฑ๋„: ์ž…๋ ฅ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ, ์—๋Ÿฌ ์ฒ˜๋ฆฌ, ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„ 

๋Œ“๊ธ€๋‚จ๊ธฐ๊ธฐ