Dandy Now!
  • [Flutter] "모두가 할 수 있는 플러터 UI 입문" - 쇼핑카트 앱 만들기 | StatefulWidget, Stack, Positioned, CupertinoAlertDialog
    2022년 02월 07일 00시 42분 08초에 업로드 된 글입니다.
    작성자: DandyNow
    728x90
    반응형

    "최주호, 정호준, & 정동진. (2021). 모두가 할 수 있는 플러터 UI 입문. 앤써북"으로 플러터 공부를 하고 있다. 8장에서는 쇼핑카드 앱을 만들었다. 이번 장에서는 StatefulWidget과 SatelessWidget을 심도 깊게 다루었다. StatefulWidget은 변경 가능한 상태를 가진 위젯이다. 사용자와의 상호작용에 의해 변경되는 경우에 사용한다. 그래서 final변수가 아닌 일반적인 변수를 가진다. SatelessWidget은 앱이 최초 실행될 때 단 한번 그려진다. 반면 StatefulWidget은 build 함수가 실행되면 계속해서 다시 그려진다. setState 함수를 통해서 상태 변수를 변경하게 되면 build 함수가 다시 실행된다. 앱에는 다시 그려져야 하는 부분과 그렇지 않은 부분이 있다. 매번 전체 앱이 다시 그려진다면 성능면에서 손해이다. 따라서 상태가 있는 StatefulWidget과 상태가 없는 SatelessWidget을 BuildContext를 이용해 분리하여 사용한다. 그 밖에도 Stack, Positioned, CupertinoAlertDialog 위젯을 다루었다.

     

    코드 작성을 완료하고 실행해 보았는데 [그림 1]과 같이 앱바의 색상이 책의 그림과 달랐다. 그리고 아래쪽에 overflow가 발생했다. 이 문제를 해결하기 위해 main.dart 코드를 조금 수정하였고 [그림 2]와 같이 완성하였다. "Add to Cart" 버튼을 누르면 CupertinoAlertDialog 위젯인 "장바구니에 담으시겠습니까?"가 팝업된다.

     

    [그림 1] 코드 작성 완료 후 몇 가지 문제

     

    [그림 2] 완성된 쇼핑카트 앱

     

    // main.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_shoppingcart/components/shoppingcart_header.dart';
    import 'package:flutter_shoppingcart/components/shoppingcart_detail.dart';
    import 'package:flutter_shoppingcart/theme.dart';
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          debugShowCheckedModeBanner: false, // 우측 상단 DEBUG 표시 안보이게 하였다.
          theme: theme(),
          home: ShoppingCartPage(),
        );
      }
    }
    
    class ShoppingCartPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: _buildShoppingCartAppBar(),
          body: SingleChildScrollView(
            // 스크롤을 달아서 overflow 문제 해결하였다.
            child: Column(
              children: [
                ShoppingCartHeader(),
                ShoppingCartDetail(),
              ],
            ),
          ),
        );
      }
    
      AppBar _buildShoppingCartAppBar() {
        return AppBar(
          backgroundColor: Color(0xFFeeeeee), // 배경색 코드 추가하였다.
          leading: IconButton(
            icon: Icon(
              Icons.arrow_back,
              color: Colors.black, // 화살표 색상 변경 코드 추가하였다.
            ),
            onPressed: () {},
          ),
          actions: [
            IconButton(
                onPressed: () {},
                icon: Icon(Icons.shopping_cart,
                    color: Colors.black)), // 쇼핑카트 색상 변경 코드 추가하였다.
            SizedBox(width: 16),
          ],
          elevation: 0.0,
        );
      }
    }

     

    // constants.dart
    import 'package:flutter/material.dart';
    
    const kPrimaryColor = Color(0xFFeeeeee); // 앱 브랜드 색
    const kSecondaryColor = Color(0xFFc6c6c6); // 앱 브랜드 색
    const kAccentColor = Color(0xFFff7643); // 앱 브랜드 색

     

    // theme.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_shoppingcart/constants.dart';
    
    ThemeData theme() {
      return ThemeData(
        primaryColor: kPrimaryColor,
        scaffoldBackgroundColor: kPrimaryColor,
      );
    }

     

    // components/shoppingcart_header.dart
    import 'package:flutter/material.dart';
    import 'package:flutter/cupertino.dart';
    import 'package:flutter_shoppingcart/constants.dart';
    
    // StatefulWidget은 상태가 있는 위젯이다. 상태가 있는 위젯은 앱이 실행되고 난 뒤에도 상태가 변경되면(사용자와의 상호작용을 통해) 그림을 다시 그릴 수 있다.
    // 그림이 다시 그려진다는 것은 build 함수가 다시 실행된다는 의미이다.
    class ShoppingCartHeader extends StatefulWidget {
      @override
      _ShoppingCartHeaderState createState() => _ShoppingCartHeaderState();
    }
    
    class _ShoppingCartHeaderState extends State<ShoppingCartHeader> {
      int selectedId = 0;
    
      List<String> selectedPic = [
        "assets/p1.jpg",
        "assets/p2.jpg",
        "assets/p3.jpg",
        "assets/p4.jpg",
      ];
    
      @override
      // BuildContext는 어떤 위젯을 다시 그려야 할지 참조하기 위한 것이다(Build는 짓다, Context는 전후사정).
      Widget build(BuildContext context) {
        return Column(
          children: [
            _buildHeaderPic(),
            _buildHeaderSelector(),
          ],
        );
      }
    
      Widget _buildHeaderPic() {
        return Padding(
          padding: const EdgeInsets.all(16.0),
          child: AspectRatio(
            aspectRatio: 5 / 3,
            child: Image.asset(
              selectedPic[selectedId],
              fit: BoxFit.cover,
            ),
          ),
        );
      }
    
      Widget _buildHeaderSelector() {
        return Padding(
          padding: const EdgeInsets.only(left: 30, right: 30, top: 10, bottom: 30),
          child: 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 id, IconData mIcon) {
        return Container(
          width: 70,
          height: 70,
          decoration: BoxDecoration(
            color: id == selectedId ? kAccentColor : kSecondaryColor,
            borderRadius: BorderRadius.circular(20),
          ),
          child: IconButton(
            icon: Icon(mIcon, color: Colors.black),
            onPressed: () {
              setState(() {
                selectedId = id;
              });
            },
          ),
        );
      }
    }

     

    // components/shoppingcart_detail.dart
    import 'package:flutter/material.dart';
    import 'package:flutter/cupertino.dart';
    import 'package:flutter_shoppingcart/constants.dart';
    
    class ShoppingCartDetail extends StatelessWidget {
      @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(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                _buildDetailNameAndPrice(),
                _buildDetailRatingAndReviewCount(),
                _buildDetailColorOptions(),
                _buildDetailButton(context),
              ],
            ),
          ),
        );
      }
    
      Widget _buildDetailNameAndPrice() {
        return Padding(
          padding: EdgeInsets.only(bottom: 10),
          child: Row(
            mainAxisAlignment:
                MainAxisAlignment.spaceBetween, // spaceBetween 이 적용되면 양 끝이 벌어진다.
            children: [
              Text(
                "Urban Soft Al 10.0",
                style: TextStyle(
                  fontSize: 18,
                  fontWeight: FontWeight.bold,
                ),
              ),
              Text(
                "\₩899,000",
                style: TextStyle(
                  fontSize: 18,
                  fontWeight: FontWeight.bold,
                ),
              )
            ],
          ),
        );
      }
    
      Widget _buildDetailRatingAndReviewCount() {
        return Padding(
          padding: 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(), // Spacer로 별 Icon과 Text 위젯을 양끝으로 벌린다(spaceBetween과 같다).
              Text("review "),
              Text("(56)", style: TextStyle(color: Colors.blue)),
            ],
          ),
        );
      }
    
      Widget _buildDetailColorOptions() {
        return Padding(
          padding: EdgeInsets.only(bottom: 20),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text("Color Options"),
              SizedBox(height: 10),
              Row(
                children: [
                  // 동일한 색상 아이콘을 재사용하기 위해 함수로 관리한다.
                  _buildDetailIcon(Colors.black),
                  _buildDetailIcon(Colors.green),
                  _buildDetailIcon(Colors.orange),
                  _buildDetailIcon(Colors.grey),
                  _buildDetailIcon(Colors.white),
                ],
              )
            ],
          ),
        );
      }
    
      // 만약 다른 화면에서도 재사용하려면 함수가 아닌 공통 컴포넌트 위젯으로 관리하면 된다.
      Widget _buildDetailIcon(Color mColor) {
        return Padding(
          padding: EdgeInsets.only(right: 10),
          // Stack 위젯은 여러 위젯을 겹칠 때 사용한다.
          // Stack의 첫 번째 Container 위젯 위에 Positioned 위젯이 올라가는 형태이다.
          child: Stack(
            children: [
              Container(
                width: 50,
                height: 50,
                decoration: BoxDecoration(
                  color: Colors.white,
                  border: Border.all(),
                  shape: BoxShape.circle,
                ),
              ),
              // Positioned 위젯은 여러 위젯이 겹쳐 있을 때 하위 위젯의 위치를 제어할 때 사용한다.
              Positioned(
                left: 5,
                top: 5,
                child: ClipOval(
                  child: Container(
                    color: mColor,
                    width: 40,
                    height: 40,
                  ),
                ),
              )
            ],
          ),
        );
      }
    
      // 만약 다른 화면에서도 재사용하려면 함수가 아닌 공통 컴포넌트 위젯으로 관리하면 된다.
      Widget _buildDetailButton(BuildContext context) {
        return Align(
          child: TextButton(
            onPressed: () {
              showCupertinoDialog( // showCupertinoDialog 함수는 CupertinoAlertDialog 위젯이 화면에 팝업되게 한다.
                context: context,
                builder: (context) => CupertinoAlertDialog( // CupertinoAlertDialog 위젯은 iOS 스타일의 경고 대화 상자이다.
                  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),
            ),
          ),
        );
      }
    }
    728x90
    반응형
    댓글