inblog logo
|
jjack1
    Flutter

    [Flutter] flutter_shoppingcart

    최재원's avatar
    최재원
    Jul 30, 2025
    [Flutter] flutter_shoppingcart
    Contents
    1. 위젯상태 변경 riverpod뷰 모델을 통해 상태와 뷰를 따로 분리해서 다뤄야 한다

    코드

    https://github.com/flutter-lec/flutter_shoppingcart

    1. 위젯

    Stack

    용도
    • 여러 위젯을 겹쳐서 배치할 때 사용. 자식 위젯을 Z축(앞뒤 순서) 기준으로 쌓음.
    어디서 사용
    • 배경 위에 텍스트나 버튼을 겹치고 싶을 때
    • 이미지 위에 아이콘 배치
    • 커스텀 뱃지, 오버레이 UI 구현 등
    기본 정렬
    • 자식 위젯들을 기본적으로 좌측 상단에 정렬함.
    레이아웃 (가로/세로 제약 및 동작)
    • Stack은 자식 중 가장 큰 위젯을 기준으로 크기 결정
    • fit 속성에 따라 크기 결정 방식이 달라짐
      • StackFit.loose (기본값): 자식 위젯이 가능한 크기를 가짐
      • StackFit.expand: Stack이 주어진 영역을 모두 채우게 함
    • 자식 위젯을 정밀히 위치 지정하려면 Positioned 위젯 사용
    • 자식이 Positioned 없이 배치되면 좌상단에 고정됨
    속성
    • children → List<Widget> | Stack에 겹쳐서 배치할 자식 위젯 목록 (필수)
    • alignment → AlignmentGeometry | Positioned를 사용하지 않은 자식들의 기본 정렬 위치 (기본값: AlignmentDirectional.topStart)
    • fit → StackFit | Stack이 자식 위젯들의 크기를 어떻게 반영할지 결정 (기본값: StackFit.loose)
    • clipBehavior → Clip | Stack 경계를 넘는 자식을 잘라낼지 여부 (기본값: Clip.hardEdge)
    • textDirection → TextDirection? | alignment가 방향성 영향을 받을 경우 사용 (예: LTR, RTL)
    import 'package:flutter/material.dart'; class ColorIcon extends StatelessWidget { double rGap; Color color; ColorIcon({this.rGap = 0.0, this.color = Colors.white}); @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.only(right: rGap), child: Stack( children: [ Container( width: 50, height: 50, decoration: BoxDecoration( border: Border.all(), shape: BoxShape.circle, color: Colors.white, ), ), Positioned( top: 5, left: 5, child: ClipOval( child: Container( width: 40, height: 40, decoration: BoxDecoration(color: color), ), ), ), ], ), ); } }
    notion image
    • 선이 있는 컨테이너 위로(Z축) 색이 있는 컨테이너가 있다
     

    Positioned

    용도
    • Stack 내부에서 자식 위젯의 정확한 위치를 지정할 때 사용.
    어디서 사용
    • Stack 내에서 위젯을 상단/하단/좌우에 고정할 때
    • 특정 위치(좌표)에 버튼, 텍스트 등을 배치할 때
    기본 정렬
    • 별도 정렬 없음. **명시적으로 지정된 위치 값(left, right 등)**을 따름.
    레이아웃 (가로/세로 제약 및 동작)
    • 반드시 Stack 위젯 내부에서만 사용 가능
    • top, left, right, bottom 값을 통해 위치 지정
    • 네 방향 값을 조합해 크기를 고정하거나 자동으로 계산 가능
    • 방향값이 없으면 부모(Stack)의 기본 alignment를 따름
    속성
    • child → Widget | 위치를 지정할 실제 자식 위젯
    • top → double? | Stack 상단으로부터의 거리
    • left → double? | Stack 왼쪽으로부터의 거리
    • right → double? | Stack 오른쪽으로부터의 거리
    • bottom → double? | Stack 하단으로부터의 거리
    • width → double? | 자식 위젯의 너비를 고정할 경우 사용
    • height → double? | 자식 위젯의 높이를 고정할 경우 사용

    RichText

    용도
    • 하나의 텍스트 블록에서 다양한 스타일(색상, 굵기 등)을 섞어 표현할 때 사용.
    어디서 사용
    • 문장 안의 특정 단어에만 색, 굵기 등 스타일을 다르게 줄 때
    • 하이퍼링크, 강조, 다국어 조합이 있는 UI에 적합
    기본 정렬
    • 텍스트는 기본적으로 왼쪽 정렬, 위에서 아래로 흐름.
    레이아웃 (가로/세로 제약 및 동작)
    • 상위 위젯이 주는 제약 안에서 텍스트 크기에 맞게 줄바꿈하며 확장
    • 내부 텍스트는 TextSpan으로 구성되어 스타일별로 분할 표현
    속성
    • text → InlineSpan | 표시할 텍스트와 스타일 정보. 보통 TextSpan 사용
    • textAlign → TextAlign | 텍스트 정렬 방식 (기본값: TextAlign.start)
    • textDirection → TextDirection? | 텍스트의 방향 (예: LTR, RTL). 필요 시 명시
    • maxLines → int? | 최대 줄 수. 초과 시 잘림
    • overflow → TextOverflow | 텍스트가 넘칠 때 처리 방식 (기본값: TextOverflow.clip)
    • softWrap → bool | 줄바꿈 여부 설정 (기본값: true)
    • strutStyle → StrutStyle? | 줄 간격 및 높이 정렬 설정
    • textScaleFactor → double | 텍스트 크기 배율 (기본값: 1.0)
    RichText( text: TextSpan( style: TextStyle(fontSize: 18), children: [ TextSpan(text: "review"), WidgetSpan(child: SizedBox(width: 5)), TextSpan( text: "(26)", style: TextStyle(color: Colors.blue), ), ], ), ),
    notion image

    TextSpan

    용도
    • RichText에서 스타일이 다른 텍스트 조각을 구성할 때 사용.
    • 텍스트와 텍스트 스타일을 정의하는 객체.
    어디서 사용
    • RichText의 text 속성에 사용
    • 여러 TextSpan을 중첩하여 다양한 텍스트 스타일을 한 줄/블록에 표현 가능
    기본 정렬
    • 정렬은 RichText의 textAlign 속성을 따름
    레이아웃 (가로/세로 제약 및 동작)
    • 자체적으로 레이아웃 제약 없음
    • RichText가 제약 조건을 제시하며, 내부에서는 TextStyle에 따라 표현
    속성
    • text → String? | 표시할 실제 문자열
    • style → TextStyle? | 텍스트에 적용할 스타일 (색상, 폰트, 크기 등)
    • children → List<InlineSpan>? | 중첩된 하위 텍스트 조각들 (재귀적으로 다른 TextSpan을 포함 가능)
    • recognizer → GestureRecognizer? | 탭 등 제스처 인식기 (예: 링크처럼 클릭 가능하게 만들기)
    • semanticsLabel → String? | 접근성(스크린 리더)용 대체 라벨
    RichText( text: TextSpan( style: TextStyle(fontSize: 18), children: [ TextSpan(text: "review"), WidgetSpan(child: SizedBox(width: 5)), TextSpan( text: "(26)", style: TextStyle(color: Colors.blue), ), ], ), ),
    notion image

    WidgetSpan

    용도
    • RichText 안에서 **텍스트 줄에 위젯(아이콘, 버튼 등)**을 텍스트처럼 삽입할 때 사용.
    어디서 사용
    • 텍스트 중간에 아이콘, 이미지, 버튼 등을 자연스럽게 넣고 싶을 때
    • RichText의 text 속성 내에서 TextSpan과 함께 사용 가능
    기본 정렬
    • 기본적으로 텍스트 라인 기준으로 정렬되며, alignment로 조절 가능
    레이아웃 (가로/세로 제약 및 동작)
    • RichText의 레이아웃 제약 안에서, child 위젯이 텍스트 줄과 함께 흐름에 맞게 배치됨
    • 텍스트처럼 줄바꿈에 포함되며, 위젯의 크기에 따라 줄 높이가 조절될 수 있음
    • 텍스트처럼 inline 흐름을 가짐 (단, 복잡한 레이아웃은 Row 등 사용 추천)
    속성
    • child → Widget | 텍스트 사이에 삽입할 위젯 (예: Icon, Image, Container 등)
    • alignment → PlaceholderAlignment | 텍스트 줄 내에서 위젯의 수직 정렬 위치
      • (기본값: PlaceholderAlignment.bottom)
        → 주요 옵션: baseline, middle, top, bottom, aboveBaseline, belowBaseline
    • baseline → TextBaseline? | alignment이 baseline 관련일 때 기준이 되는 베이스라인 설정
      • (예: TextBaseline.alphabetic)
    • style → TextStyle? | 위젯이 차지하는 영역에 적용되는 텍스트 스타일 (공백 등 처리에 영향 있음)
    RichText( text: TextSpan( style: TextStyle(fontSize: 18), children: [ TextSpan(text: "review"), WidgetSpan(child: SizedBox(width: 5)), TextSpan( text: "(26)", style: TextStyle(color: Colors.blue), ), ], ), ),
    notion image

    상태 변경 riverpod

    💡
    데이터 박스가 불변해야 하는 이유 박스의 내부가 변경되면 플러터 엔진이 뭐가 바뀌었는지 확인을 해야 한다 그냥 박스를 통으로 바꾸면 뭐가 바뀌었는지 찾을 필요 없고 그냥 박스가 통으로 바뀌었다는 것을 바로 알 수 있다 원래 박스와 내가 변경한 내용을 비교할 수 없다
    같은 박스에서 내부 상태만 변경하면 원래 값이 사라지기 때문에 비교가 불가능하다 하지만 박스를 그냥 통으로 바꾸면 그냥 박스가 바뀌었다는 것을 알 수 있기 때문에 변경을 감지할 수 있다
    요약 기존 데이터가 없으면 변경 감지가 불가능하다 그래서 기존 데이터를 수정하는 방법을 사용하면 안된다
    깊은 복사 버전을 사용하자

    뷰 모델을 통해 상태와 뷰를 따로 분리해서 다뤄야 한다

    뷰 모델

    import 'package:flutter_riverpod/flutter_riverpod.dart'; // 1. 창고 데이터 타입 (원시타입이면 안만들어도 됨) class SelectorModel { final List<String> images; // 불변!! final int selectedId; SelectorModel(this.images, this.selectedId); SelectorModel copyWith({ List<String>? images, int? selectedId, }) { return SelectorModel( images ?? this.images, selectedId ?? this.selectedId, ); } // 선택된 이미지 반환 String selectedImage() => images[selectedId]; // 현재 index가 선택된 index인지 여부 반환 bool isSelected(int id) => selectedId == id; } /// 2. 창고 (상태와 행위(변경로직)를 가진다) /// - Notifier를 상속하여 상태를 관리하고 변경할 수 있음 /// - build(): 초기 상태 설정 /// - onClick(): 새로운 상태로 갱신 class HomeVM extends Notifier<SelectorModel> { @override SelectorModel build() { return SelectorModel(["assets/p1.jpeg", "assets/p2.jpeg", "assets/p3.jpeg", "assets/p4.jpeg"], 0); } void onClick(int id) { print(id); // 1. 기존 값 변경 (rebuild 안됨) final 지우고 테스트 // state.selectedId = id; // 2. 기존 값 불변 유지 -> 깊은 복사 (rebuild 됨) - copyWith 라는 메서드를 일반적으로 사용 state = state.copyWith(selectedId: id); // SelectorModel prevModel = state; // 100번지 // SelectorModel nextModel = SelectorModel(prevModel.images, id); // 200번지 // state = nextModel; } } // 3. 창고 관리자 final homeProvider = NotifierProvider<HomeVM, SelectorModel>(() { return HomeVM(); });
    • 모델에 있는 필드 값들은 나중에 서버의 DTO 와 동일하게 만들면 된다
    import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../page/home_vm.dart'; import 'selector_button.dart'; class SelectorHeader extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { SelectorModel model = ref.watch(homeProvider); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ ClipRRect( borderRadius: BorderRadius.circular(5), child: AspectRatio( aspectRatio: 5 / 3, child: Image.asset("${model.selectedImage()}", fit: BoxFit.cover), ), ), Padding( padding: const EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ SelectorButton(id: 0, icon: Icon(Icons.directions_bike)), SelectorButton(id: 1, icon: Icon(Icons.motorcycle)), SelectorButton(id: 2, icon: Icon(Icons.directions_car_filled)), SelectorButton(id: 3, icon: Icon(Icons.airplanemode_on)), ], ), ), ], ), ); } }
    • 뷰에서는 데이터를 읽거나, 변경하는 코드만 가지고 있는다
    출처: 만들면서 배우는 플러터 앱 프로그래밍
     
    Share article
    Contents
    1. 위젯상태 변경 riverpod뷰 모델을 통해 상태와 뷰를 따로 분리해서 다뤄야 한다

    jjack1

    RSS·Powered by Inblog