Flutter

[Flutter] 연습하기 5 - Flutter shopping cart app

연화 2025. 1. 14. 17:45

 

안녕하세요! 이번 포스팅에서는 플러터(Flutter)를 활용해 쇼핑카트 앱을 만들어보는 과정을 소개하려고 합니다.
플러터의 다양한 UI 위젯과 상태 관리 방식을 익힐 수 있었어요.

 

1. lib/constants.dart 파일 만들기 
2. lib/theme.dart 파일 만들기 
3. main.dart 기본 코드 설계하기 
4. 쇼핑카트 헤더 만들기 
5. main 앱바 추가 
6. 쇼핑카트 바디 만들기

 

 

1. 앱의 테마 설정

앱의 색상과 테마는 constants.dart와 theme.dart 파일에 정의하여 유지보수가 쉽도록 설계했습니다. 이를 통해 전체 앱의 색상 일관성을 유지할 수 있습니다.

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:shopping_cart_app/constants.dart';

ThemeData mTheme() {
  return ThemeData(
    primarySwatch: kPrimaryColor,
    scaffoldBackgroundColor: kPrimaryColor,
  );
}

 

2. 헤더 UI 구현

쇼핑카트의 헤더는 메인 이미지를 보여주고, 네 개의 네비게이션 버튼을 통해 이미지를 전환하는 기능을 포함하고 있습니다.

  • setState를 이용해 버튼 클릭 시 선택된 이미지를 변경하고, Image.asset 위젯을 사용해 동적으로 이미지를 업데이트했습니다.
  • 네비게이션 버튼은 플러터의 IconButton을 활용해 구현했습니다.
main.dart - AppBar 메서드
import 'package:flutter/material.dart';
import 'package:flutter_shopping_cart_app/components/shopping_cart_body.dart';
import 'package:flutter_shopping_cart_app/components/shopping_cart_header.dart';
import 'package:flutter_shopping_cart_app/theme.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: mTheme(),
      home: ShoppingCartPage(),
    );
  }
}

// 여기는 페이지
class ShoppingCartPage extends StatelessWidget {
  const ShoppingCartPage({super.key});

  @override
  Widget build(BuildContext context) {
    return SafeArea(
        child: Scaffold(
      appBar: _buildShoppingCartAppBar(),
      body: Stack(
        alignment: Alignment.bottomCenter,
        children: [
          ShoppingCartHeader(),
          ShoppingCartBody(),
        ],
      ),
    ));
  }

  AppBar _buildShoppingCartAppBar() {
    return AppBar(
      leading: IconButton(
        onPressed: () {},
        icon: Icon(Icons.arrow_back),
      ),
      actions: [
        IconButton(
          onPressed: () {},
          icon: Icon(Icons.shopping_cart),
        )
      ],
    );
  }
}

 

shopping_cart_header.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_shopping_cart_app/constants.dart';

class ShoppingCartHeader extends StatefulWidget {
  const ShoppingCartHeader({super.key});

  @override
  State<ShoppingCartHeader> createState() => _ShoppingCartHeaderState();
}

class _ShoppingCartHeaderState extends State<ShoppingCartHeader> {
  // 1. 상태
  int selectedId = 0;
  List<String> selectedPic = [
    'assets/p1.jpeg',
    'assets/p2.jpeg',
    'assets/p3.jpeg',
    'assets/p4.jpeg',
  ];

  // 상태는 행위를 통해 변경해야 한다.
  void changeSelectedId(int sId) {
    setState(() {
      selectedId = sId;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Padding(
          padding: const EdgeInsets.all(16.0),
          child: AspectRatio(
            aspectRatio: 5 / 3,
            child: Image.asset(
              selectedPic[selectedId],
              fit: BoxFit.cover,
            ),
          ),
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            _buildHeaderSelectorButton(0, Icons.directions_bike),
            _buildHeaderSelectorButton(1, Icons.motorcycle),
            _buildHeaderSelectorButton(2, CupertinoIcons.car_detailed),
            _buildHeaderSelectorButton(3, CupertinoIcons.airplane),
          ],
        )
      ],
    );
  }

  Widget _buildHeaderSelectorButton(int sId, IconData mIcon) {
    return Container(
      width: 70,
      height: 70,
      decoration: BoxDecoration(
        color: (selectedId == sId) ? kAccentColor : kSecondaryColor,
        borderRadius: BorderRadius.circular(20.0),
      ),
      child: IconButton(
        onPressed: () {
          changeSelectedId(sId);
        },
        icon: Icon(mIcon),
        color: Colors.black,
      ),
    );
  }
} // end of header

 

3. 바디 UI 구현

바디 영역은 상품 정보를 직관적으로 전달할 수 있도록 구성되었습니다.

  • 상품 이름, 가격, 별점, 리뷰 수를 보여주는 레이아웃을 Row와 Column 위젯을 조합해 설계했습니다.
  • 색상 옵션은 동그란 버튼 형식으로 제공되며, 선택 시 직관적으로 확인할 수 있도록 구현했습니다.
  • 마지막으로, 장바구니 버튼을 통해 상품을 추가할 수 있는 인터페이스를 제공했습니다.
shopping_cart_body.dart
import 'package:flutter/material.dart';
import 'package:flutter_shopping_cart_app/constants.dart';

class ShoppingCartBody extends StatefulWidget {
  const ShoppingCartBody({super.key});

  @override
  State<ShoppingCartBody> createState() => _ShoppingCartBodyState();
}

class _ShoppingCartBodyState extends State<ShoppingCartBody> {
  @override
  Widget build(BuildContext context) {
    return Container(
      height: 350,
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.only(
          topLeft: Radius.circular(30.0),
          topRight: Radius.circular(30.0),
        ),
      ),
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 20.0),
        child: Column(
          children: [
            const SizedBox(height: 30.0),
            _buildBodyNameAndPrice(),
            const SizedBox(height: 10.0),
            _buildBodyRatingAndReviewCount(4, 45),
            const SizedBox(height: 20.0),
            _buildBodyColorOptions([
              Colors.black,
              Colors.green,
              Colors.orange,
              Colors.white,
              Colors.blueAccent,
            ]),
            const SizedBox(height: 30.0),
            _buildBodyButton(),
          ],
        ),
      ),
    );
  }

  // end of build
  Widget _buildBodyNameAndPrice() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Text(
          'Urban Soft AL 10.0',
          style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
        ),
        Text(
          '\$699',
          style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
        ),
      ],
    );
  }

  // 2. 별점 리뷰 카운트
  Widget _buildBodyRatingAndReviewCount(int counting, int reviews) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        _buildStarCounting(counting),
        Row(
          children: [
            Text(
              'review',
              style: TextStyle(color: Colors.grey),
            ),
            Text(
              '(${reviews})',
              style: TextStyle(color: Colors.blue),
            )
          ],
        )
      ],
    );
  }

  Widget _buildStarCounting(int counting) {
    return Row(
      children: [
        for (int i = 1; i <= counting; i++)
          Icon(Icons.star, color: Colors.amber),
        for (int v = 0; v < 5 - counting; v++)
          Icon(Icons.star, color: Colors.black12),
      ],
    );
  }

  // 3. 색상 옵션 선택
  Widget _buildBodyColorOptions(List<Color> colors) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          'Color Options',
          style: TextStyle(color: Colors.black54, fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 10.0),
        Row(
          children: [
            ...List.generate(colors.length, (index) {
              return _buildColorOption(colors[index]);
            })
          ]
          //for (Color c in colors) _buildColorOption(c),
          ,
        )
      ],
    );
  }

  // 4. 색상 옵션 위젯을 만드는 함수
  Widget _buildColorOption(Color color) {
    return Padding(
      padding: EdgeInsets.only(right: 10),
      child: Stack(
        alignment: Alignment.center,
        children: [
          // positioned 위젯 활용
          Positioned(
            child: Container(
              width: 50,
              height: 50,
              decoration: BoxDecoration(
                  color: Colors.white,
                  border: Border.all(color: Colors.grey, width: 3),
                  borderRadius: BorderRadius.circular(50.0)),
            ),
          ),
          Positioned(
              child: Container(
            width: 40,
            height: 40,
            decoration: BoxDecoration(
                color: color, borderRadius: BorderRadius.circular(50.0)),
          ))
        ],
      ),
    );
  }

  // 5. 버튼 만들기
  Widget _buildBodyButton() {
    return TextButton(
      onPressed: () {},
      child: Text('Add to Cart'),
    );
  }
} // end of body

 

4. 코드 리팩터링

바디 영역의 반복되는 코드를 줄이고, 컴포넌트를 분리하여 유연하고 재사용 가능한 구조로 구성하였습니다.

  • 상품 이름, 가격, 별점, 리뷰 수, 색상 옵션 등과 같은 상품 데이터를 효율적으로 관리하기 위해 Product 모델을 정의했습니다.
  • 리팩터링 과정에서 반복되는 UI 코드(예: 별점 생성, 색상 옵션 생성 등)를 함수나 컴포넌트로 추출했습니다.
별점 리뷰 카운트 위젯
Widget _buildBodyRatingAndReviewCount(int counting, int reviews) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        _buildStarCounting(counting),
        Row(
          children: [
            Text(
              'review',
              style: TextStyle(color: Colors.grey),
            ),
            Text(
              '(${reviews})',
              style: TextStyle(color: Colors.blue),
            )
          ],
        )
      ],
    );
  }

  Widget _buildStarCounting(int counting) {
    return Row(
      children: [
        for (int i = 1; i <= counting; i++)
          Icon(Icons.star, color: Colors.amber),
        for (int v = 0; v < 5 - counting; v++)
          Icon(Icons.star, color: Colors.black12),
      ],
    );
  }

 

색상 옵션 위젯
// 3. 색상 옵션 선택
  Widget _buildBodyColorOptions(List<Color> colors) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          'Color Options',
          style: TextStyle(color: Colors.black54, fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 10.0),
        Row(
          children: [
            ...List.generate(colors.length, (index) {
              return _buildColorOption(colors[index]);
            })
          ]
          //for (Color c in colors) _buildColorOption(c),
          ,
        )
      ],
    );
  }

  // 4. 색상 옵션 위젯을 만드는 함수
  Widget _buildColorOption(Color color) {
    return Padding(
      padding: EdgeInsets.only(right: 10),
      child: Stack(
        alignment: Alignment.center,
        children: [
          // positioned 위젯 활용
          Positioned(
            child: Container(
              width: 50,
              height: 50,
              decoration: BoxDecoration(
                  color: Colors.white,
                  border: Border.all(color: Colors.grey, width: 3),
                  borderRadius: BorderRadius.circular(50.0)),
            ),
          ),
          Positioned(
              child: Container(
            width: 40,
            height: 40,
            decoration: BoxDecoration(
                color: color, borderRadius: BorderRadius.circular(50.0)),
          ))
        ],
      ),
    );
  }
💡   List.generate  지정된 길이만큼 리스트를 생성하면서, 각 항목에 대해 동적으로 값을 생성
      스프레드 연산자 (...)  리스트의 각 요소를 부모 리스트에 풀어주는 역할
children: [
    ...List.generate(colors.length, (index) {
      return _buildColorOption(colors[index]);
    })
]

 

최종 화면

 

 

이번 쇼핑카트 앱을 만들면서 플러터의 UI 구성 원리와 상태 관리에 대해 이해할 수 있었습니다.
특히, setState를 이용한 동적 UI 변화와 Row, Column 위젯을 활용한 레이아웃 구성 방법은 실제 프로젝트에 바로 응용할 수 있을 만큼 큰 도움이 된 것 같아요.

다음 포스팅에서도 플러터의 다양한 기능과 패턴을 공부해 가져오도록 하겠습니다!

 

 

👀 플러터 UI 실습 예제를 더 알고 싶어요!

 

[Flutter] 연습하기 4 - Flutter login app

안녕하세요! 오늘은 Flutter를 활용하여 간단한 로그인 화면을 구현하는 방법을 공유하려 합니다. 이 프로젝트는 Flutter를 처음 배우는 분들도 쉽게 따라할 수 있도록 구성되어 있습니다. 기본적인

dev-yeonwha.tistory.com

 

 

[Flutter] 연습하기3 - Flutter profile app

이 블로그 포스팅에서는 Flutter를 이용해 프로필 페이지를 구현하는 방법을 설명합니다. 각 섹션을 나누어 테마 설정부터 프로필 헤더, 버튼, 그리고 탭을 사용하는 방식까지 순차적으로 구성해

dev-yeonwha.tistory.com

 

 

아래의 문헌을 참고하여 작성된 포스팅입니다.
최주호, 김근호, 이지원(공저) 『만들면서 배우는 플러터 앱 프로그래밍』, 앤써북, 2023.