1. 기본 세팅

assets 폴더를 만든 후 사진을 넣는다.

pubspec.yml 을 세팅 후 pub get 을 누른다.
2. 테마 설정하기
constants.dart
import 'package:flutter/material.dart';
const kPrimaryColor = MaterialColor(
  0xFFeeeeee,
  <int, Color>{
    50: Color(0xFFeeeeee),
    100: Color(0xFFeeeeee),
    200: Color(0xFFeeeeee),
    300: Color(0xFFeeeeee),
    400: Color(0xFFeeeeee),
    500: Color(0xFFeeeeee),
    600: Color(0xFFeeeeee),
    700: Color(0xFFeeeeee),
    800: Color(0xFFeeeeee),
    900: Color(0xFFeeeeee),
  },
);
const kSecondaryColor = Color(0xffc6c6c6);// 기본 버튼색
const kAccentColor = Color(0xFFff7643);theme.dart
import 'package:flutter/material.dart';
import 'package:shoping_app/constants.dart';
ThemeData theme(){
  return ThemeData(
    primarySwatch: kPrimaryColor,
    scaffoldBackgroundColor: kPrimaryColor ,
  );
}main.dart
import 'package:flutter/material.dart';
import 'package:shoping_app/theme.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: theme(),
    );
  }
}
3. ShoppingCartHeader 만들기
import 'package:flutter/material.dart';
import 'package:shoping_app/theme.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: theme(),
      home: Scaffold(
        body: Column(
          children: [
            SizedBox(),
          ],
        ),
      ),
    );
  }
}

SizedBox 컴포넌트를 분리 후 외부 클래스로 만든다.
components/shoping_cart_header.dart
import 'package:flutter/material.dart';
class ShoppingCartHeader extends StatelessWidget {
  const ShoppingCartHeader({
    super.key,
  });
  @override
  Widget build(BuildContext context) {
    return SizedBox();
  }
}

외부로 분리한 컴퍼넌트에 alt+enter를 눌러 Stateful 위젯으로 변경한다.
shopping_cart_header.dart
import 'package:flutter/material.dart';
class ShoppingCartHeader extends StatefulWidget {
  @override
  State<ShoppingCartHeader> createState() => _ShoppingCartHeaderState();
}
class _ShoppingCartHeaderState extends State<ShoppingCartHeader> {
  int selectedId = 0;
  List<String> selectedPic = [
    "assets/p1.jpeg",
    "assets/p2.jpeg",
    "assets/p3.jpeg",
    "assets/p4.jpeg",
  ];
  @override
  Widget build(BuildContext context) {
    return SizedBox();
  }
}
동일한 방법으로 ShoppingCartDetail 도 만든다.
shopping_cart_detail.dart
import 'package:flutter/material.dart';
class ShoppingCartHeader extends StatefulWidget {
  @override
  State<ShoppingCartHeader> createState() => _ShoppingCartHeaderState();
}
class _ShoppingCartHeaderState extends State<ShoppingCartHeader> {
  int selectedId = 0;
  List<String> selectedPic = [
    "assets/p1.jpeg",
    "assets/p2.jpeg",
    "assets/p3.jpeg",
    "assets/p4.jpeg",
  ];
  @override
  Widget build(BuildContext context) {
    return SizedBox();
  }
}
main.dart
import 'package:flutter/material.dart';
import 'package:shoping_app/theme.dart';
import 'components/shopping_cart_detail.dart';
import 'components/shopping_cart_header.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: theme(),
      home: Scaffold(
        body: Column(
          children: [
            ShoppingCartHeader(),
            ShoppingCartDetail(),
          ],
        ),
      ),
    );
  }
}4. AppBar 만들기
main.dart
import 'package:flutter/material.dart';
import 'package:shoping_app/theme.dart';
import 'components/shopping_cart_detail.dart';
import 'components/shopping_cart_header.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: theme(),
      home: Scaffold(
        appBar: AppBar(
          leading: IconButton(
            icon: Icon(Icons.arrow_back),
            onPressed: () {},
          ),
          actions: [
            IconButton(
              icon: Icon(Icons.shopping_cart),
              onPressed: () {},
            ),
            SizedBox(width: 16),
          ],
          elevation: 0.0,
        ),
        body: Column(
          children: [
            ShoppingCartHeader(),
            ShoppingCartDetail(),
          ],
        ),
      ),
    );
  }
}

AppBar를 별도로 분리한다.
main.dart
import 'package:flutter/material.dart';
import 'package:shoping_app/theme.dart';
import 'components/shopping_cart_detail.dart';
import 'components/shopping_cart_header.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: theme(),
      home: Scaffold(
        appBar: _buildShoppingCarAppBar(),
        body: Column(
          children: [
            ShoppingCartHeader(),
            ShoppingCartDetail(),
          ],
        ),
      ),
    );
  }
  AppBar _buildShoppingCarAppBar() {
    return AppBar(
        leading: IconButton(
          icon: Icon(Icons.arrow_back),
          onPressed: () {},
        ),
        actions: [
          IconButton(
            icon: Icon(Icons.shopping_cart),
            onPressed: () {},
          ),
          SizedBox(width: 16),
        ],
        elevation: 0.0,
      );
  }
}5. setState() 함수 사용하기
우선 화면 상단에 사진을 표시한다.
shopping_cart_header.dart
import 'package:flutter/material.dart';
class ShoppingCartHeader extends StatefulWidget {
  @override
  State<ShoppingCartHeader> createState() => _ShoppingCartHeaderState();
}
class _ShoppingCartHeaderState extends State<ShoppingCartHeader> {
  int selectedId = 0;
  List<String> selectedPic = [
    "assets/p1.jpeg",
    "assets/p2.jpeg",
    "assets/p3.jpeg",
    "assets/p4.jpeg",
  ];
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        AspectRatio(
          aspectRatio: 5 / 3,
          child: Image.asset(
            selectedPic[selectedId],
            fit: BoxFit.cover,
          ),
        )
      ],
    );
  }
}

AspectRatio를 별도 컴포넌트로 분리한다.
shopping_cart_header_pic.dart
import 'package:flutter/material.dart';
class HeaderPic extends StatelessWidget {
  const HeaderPic({
    super.key,
    required this.selectedPic,
    required this.selectedId,
  });
  final List<String> selectedPic;
  final int selectedId;
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: AspectRatio(
        aspectRatio: 5 / 3,
        child: Image.asset(
          selectedPic[selectedId],
          fit: BoxFit.cover,
        ),
      ),
    );
  }
}다음으로 사진을 변경할 때 사용할 버튼을 만들어보자.
shopping_cart_header.dart
import 'package:flutter/material.dart';
import 'package:shoping_app/components/shopping_cart_header_pic.dart';
import 'package:shoping_app/constants.dart';
class ShoppingCartHeader extends StatefulWidget {
  @override
  State<ShoppingCartHeader> createState() => _ShoppingCartHeaderState();
}
class _ShoppingCartHeaderState extends State<ShoppingCartHeader> {
  int selectedId = 0;
  List<String> selectedPic = [
    "assets/p1.jpeg",
    "assets/p2.jpeg",
    "assets/p3.jpeg",
    "assets/p4.jpeg",
  ];
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        HeaderPic(selectedPic: selectedPic, selectedId: selectedId),
        Row(
          children: [
            Container(
                width: 70, height: 70, 
              decoration: BoxDecoration(
                color: 0 == selectedId ? kAccentColor : kSecondaryColor,
                borderRadius: BorderRadius.circular(20),
              ),
              child: IconButton(
                icon: Icon(Icons.directions_bike),
                onPressed: () {
                  setState(() {
                    selectedId = 0 ;
                  });
                },
              ),
            ),
          ],
        ),
      ],
    );
  }
}

버튼을 컴포넌트로 분리 후 변수화 한다.
shopping_cart_header.dart
import 'package:flutter/material.dart';
import 'package:shoping_app/components/header_selector.dart';
import 'package:shoping_app/components/shopping_cart_header_pic.dart';
class ShoppingCartHeader extends StatefulWidget {
  @override
  State<ShoppingCartHeader> createState() => _ShoppingCartHeaderState();
}
class _ShoppingCartHeaderState extends State<ShoppingCartHeader> {
  int selectedId = 0;
  List<String> selectedPic = [
    "assets/p1.jpeg",
    "assets/p2.jpeg",
    "assets/p3.jpeg",
    "assets/p4.jpeg",
  ];
  //콜백함수로 부모위젯에서 자식 위젯으로 setState를 넘긴다
  void updateSelectedId(int id){
    setState(() {
      selectedId = id ;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        HeaderPic(selectedPic: selectedPic, selectedId: selectedId),
        HeaderSelector(updateSelectedId,selectedId),
      ],
    );
  }
}
자식 위젯은 stateless로 setState 함수가 없어서 부모 위젯에서 콜백 함수를 만들어서 넘긴다.
header_selector.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:shoping_app/constants.dart';
import 'header_selector_button.dart';
class HeaderSelector extends StatelessWidget {
  var updateSelectedId ;
  int selectedId ;
  HeaderSelector(this.updateSelectedId,this.selectedId);
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(left: 30,right: 30,top: 10,bottom: 30),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          HeaderSelectorButton(0,selectedId,Icons.directions_bike,updateSelectedId),
          HeaderSelectorButton(1,selectedId,Icons.motorcycle,updateSelectedId),
          HeaderSelectorButton(2,selectedId,CupertinoIcons.car_detailed,updateSelectedId),
          HeaderSelectorButton(3,selectedId,CupertinoIcons.airplane,updateSelectedId),
        ],
      ),
    );
  }
}
header_selector_button.dart
import 'package:flutter/material.dart';
import '../constants.dart';
class HeaderSelectorButton extends StatelessWidget {
  int id ;
  int selectedId;
  IconData mIcon;
  var updateSelectedId;
  HeaderSelectorButton(this.id,this.selectedId,this.mIcon, this.updateSelectedId);
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 70,
      height: 70,
      decoration: BoxDecoration(
        color: id == selectedId ? kAccentColor : kSecondaryColor,
        borderRadius: BorderRadius.circular(20),
      ),
      child: IconButton(
        icon: Icon(mIcon),
        onPressed: () {
          updateSelectedId(id);
        },
      ),
    );
  }
}


5. Shpping_cart_detail.dart 만들기
shopping_cart_detail.dart
import 'package:flutter/material.dart';
import 'package:shoping_app/components/detail_button.dart';
import 'package:shoping_app/components/detail_color_option.dart';
import 'package:shoping_app/components/detail_rating_and_review_count.dart';
import 'detail_name_and_price.dart';
class ShoppingCartDetail extends StatelessWidget {
  const ShoppingCartDetail({
    super.key,
  });
  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(40),
      ),
      child: Padding(
        padding: const EdgeInsets.all(30.0),
        child: Column(
          children: [
            DetailNameAndPrice(),
            DetailRatingAndReviewCount(),
            DetailColorOptions(),
            DetailButton(),
          ],
        ),
      ),
    );
  }
}우선 구조를 만들고 각 클래스를 만들어보자
detail_name_and_price.dart
import 'package:flutter/material.dart';
class DetailNameAndPrice extends StatelessWidget {
  const DetailNameAndPrice({super.key});
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 10),
      child: Row(
        children: [
          Text(
            "Urban Soft AL 10.0",
            style: TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.bold,
            ),
          ),
          Text(
            "\$699",
            style: TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.bold,
            ),
          )
        ],
      ),
    );
  }
}
detail_rating_and_review_count.dart
import 'package:flutter/material.dart';
class DetailRatingAndReviewCount extends StatelessWidget {
  const DetailRatingAndReviewCount({super.key});
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 20),
      child: Row(
        children: [
          Icon(Icons.star,color: Colors.yellow),
          Icon(Icons.star,color: Colors.yellow),
          Icon(Icons.star,color: Colors.yellow),
          Icon(Icons.star,color: Colors.yellow),
          Icon(Icons.star,color: Colors.yellow),
          Spacer(),
          Text("review"),
          Text("(26)",style: TextStyle(color: Colors.blue)),
        ],
      ),
    );
  }
}detail_color_option.dart
import 'package:flutter/material.dart';
import 'package:shoping_app/components/detail_icon.dart';
class DetailColorOptions extends StatelessWidget {
  const DetailColorOptions({super.key});
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 20),
      child: Column(
        children: [
          Text("Color Options"),
          SizedBox(height: 10),
          Row(
            children: [
              DetailIcon(Colors.black),
              DetailIcon(Colors.green),
              DetailIcon(Colors.orange),
              DetailIcon(Colors.grey),
              DetailIcon(Colors.white),
            ],
          ),
        ],
      ),
    );
  }
}
detail_icon.dart
import 'package:flutter/material.dart';
class DetailIcon extends StatelessWidget {
  Color mColor ;
  DetailIcon(this.mColor);
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(right: 10),
      child: Stack(
        children: [
          Container(
            width: 50,
            height: 50,
            decoration: BoxDecoration(
              color: Colors.white,
              border: Border.all(),
              shape: BoxShape.circle,
            ),
          ),
          Positioned(
            left: 5,
            top: 5,
            child: ClipOval(
              child: Container(
                color: mColor,
                width: 40,
                height: 40,
              ),
            ),
          )
        ],
      ),
    );
  }
}
Stack 위젯은 자식 위젯들을 겹쳐서 배치할 수 있는 위젯이다.  Stack의 자식 위젯 중 Positioned 위젯으로 감싸진 것들은 top, right, bottom, left 속성을 사용하여 Stack 내에서의 위치를 지정할 수 있습니다.
detail_button.dart
import 'package:flutter/material.dart';
import 'package:shoping_app/constants.dart';
class DetailButton extends StatelessWidget {
  const DetailButton({super.key});
  @override
  Widget build(BuildContext context) {
    return Align(
      child: TextButton(
       onPressed: () {},
       style: TextButton.styleFrom(
         backgroundColor: kAccentColor,
         minimumSize: Size(300, 50),
         shape: RoundedRectangleBorder(
           borderRadius: BorderRadius.circular(20),
         ),
       ),
        child: Text(
          "Add to Cart",
          style: TextStyle(color: Colors.white),
        ),
      ),
    );
  }
}

6. CupertinoAlertDialog 위젯 만들기
shopping_cart_detail.dart
import 'package:flutter/material.dart';
import 'package:shoping_app/components/detail_button.dart';
import 'package:shoping_app/components/detail_color_option.dart';
import 'package:shoping_app/components/detail_rating_and_review_count.dart';
import 'detail_name_and_price.dart';
class ShoppingCartDetail extends StatelessWidget {
  const ShoppingCartDetail({
    super.key,
  });
  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(40),
      ),
      child: Padding(
        padding: const EdgeInsets.all(30.0),
        child: Column(
          children: [
            DetailNameAndPrice(),
            DetailRatingAndReviewCount(),
            DetailColorOptions(),
            DetailButton(context),
          ],
        ),
      ),
    );
  }
}
 context를 전달하면 UI 업데이트, 의존성 주입, 화면 전환 등의 작업을 수행할 수 있다. DetailButton 위젯에서 다른 화면으로 이동해야 할 경우, context를 통해 Navigator 객체에 접근할 수 있.
detail_button.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:shoping_app/constants.dart';
class DetailButton extends StatelessWidget {
  const DetailButton(BuildContext context, {super.key});
  @override
  Widget build(BuildContext context) {
    return Align(
      child: TextButton(
        onPressed: () {
          showCupertinoDialog(
              context: context,
              builder: (context) => CupertinoAlertDialog(
                    title: Text("장바구니에 담으시겠습니까?"),
                    actions: [
                      CupertinoDialogAction(
                        child: Text("확인"),
                        onPressed: () {
                          Navigator.pop(context);
                        },
                      ),
                    ],
                  ));
        },
        style: TextButton.styleFrom(
          backgroundColor: kAccentColor,
          minimumSize: Size(300, 50),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(20),
          ),
        ),
        child: Text(
          "Add to Cart",
          style: TextStyle(color: Colors.white),
        ),
      ),
    );
  }
}

Share article