[Flutter #11] 위젯 실습: setState로 상태 변경부터 Cupertino 팝업

도경원's avatar
Sep 30, 2025
[Flutter #11] 위젯 실습: setState로 상태 변경부터 Cupertino 팝업

1. setState()로 상태 바꾸기 (선택 버튼 ↔ 이미지 교체)

핵심 아이디어
  • 상태값 selectedId가 바뀌면, build()가 다시 호출되어 selectedPic[selectedId] 이미지가 즉시 바뀐다.
  • 버튼 UI도 selectedId에 따라 배경색이 달라진다.
class ShoppingcartHeader extends StatefulWidget { @override State<ShoppingcartHeader> createState() => _ShoppingcartHeaderState(); } class _ShoppingcartHeaderState extends State<ShoppingcartHeader> { int selectedId = 0; final selectedPic = ["assets/p1.jpeg","assets/p2.jpeg","assets/p3.jpeg","assets/p4.jpeg"]; @override Widget build(BuildContext context) { return Column( children: [ // 선택된 id에 맞는 이미지 Padding( padding: const EdgeInsets.all(16.0), child: AspectRatio( aspectRatio: 5 / 3, child: Image.asset(selectedPic[selectedId], fit: BoxFit.cover), ), ), // 선택 버튼들 Padding( padding: const EdgeInsets.fromLTRB(30, 10, 30, 30), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _selector(0, Icons.directions_bike), _selector(1, Icons.motorcycle), _selector(2, CupertinoIcons.car_detailed), _selector(3, CupertinoIcons.airplane), ], ), ), ], ); } Widget _selector(int id, IconData icon) { final isSelected = id == selectedId; return Container( width: 70, height: 70, decoration: BoxDecoration( color: isSelected ? kAccentColor : kSecondaryColor, borderRadius: BorderRadius.circular(20), ), child: IconButton( icon: Icon(icon, color: Colors.black), onPressed: () => setState(() => selectedId = id), // 상태 변경 → 리빌드 ), ); } }
메모
  • setState()동기적으로 상태를 바꾸고 프레임 스케줄러에 리빌드 예약을 건다.
  • 가능한 최소 범위의 위젯에서만 호출(불필요한 리빌드 방지).
  • 리스트/맵은 참조 변경(새 인스턴스) 없이 내부만 수정하면 변경 감지가 어려울 수 있음 → 가능하면 불변 패턴 지향.

2. Stack + Positioned로 겹쳐 배치

notion image
핵심 아이디어
  • Stack은 자식들을 Z축으로 겹쳐 그린다.
  • Positioned로 자식의 절대 위치(left/top/right/bottom) 를 지정한다.
  • 원형 컨테이너 위에 작게 색원(ClipOval)을 살짝 내려놓는 UI를 구현.
Widget _buildDetailIcon(Color color) { 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(width: 40, height: 40, color: color), ), ), ], ), ); }
메모
  • 부모 Stack의 크기는 자식의 크기부모 제약(Padding/ConstrainedBox/AspectRatio 등) 로 정한다.
  • 정교한 배치를 원하면 Align/Positioned.fill/FractionalTranslation도 유용.

3. CupertinoAlertDialog + Navigator.pop() (스택에서 팝업 제거)

notion image
핵심 아이디어
  • showCupertinoDialog()는 현재 Navigator 스택 위에 다이얼로그 Route를 푸시(push) 한다.
  • 닫을 때는 Navigator.pop(context)팝(pop) 하여 스택에서 제거한다.
Widget _buildDetailButton(BuildContext context) { return Align( child: TextButton( onPressed: () { showCupertinoDialog( context: context, builder: (_) => CupertinoAlertDialog( title: const Text("장바구니에 담으시겠습니까?"), actions: [ CupertinoDialogAction( onPressed: () => Navigator.pop(context), // 팝업 닫기 = pop child: const Text("확인", style: TextStyle(color: Colors.blue)), ), ], ), ); }, style: TextButton.styleFrom( backgroundColor: kAccentColor, minimumSize: const Size(300, 50), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), ), child: const Text("Add to Cart", style: TextStyle(color: Colors.white)), ), ); }
메모
  • 머티리얼 스타일이 필요하면 showDialog + AlertDialog.
  • 바깥 탭으로 닫히게 하려면 barrierDismissible: true(머티리얼 showDialog에 존재).
  • 팝업에서 결과값을 반환하려면 Navigator.pop(context, result) 사용 → await showCupertinoDialog(...) 로 값 수신.

마무리 요약

  1. 상태 변경은 setState(): 바뀐 값만 의도적으로 갱신되도록 위젯 구조를 깔끔히 나누자.
  1. 겹쳐 배치는 Stack + Positioned: 절대 좌표/정렬로 정교하게 쌓아 그리는 패턴.
  1. 팝업은 Navigator 스택: 띄울 땐 push, 닫을 땐 pop—다이얼로그도 결국 Route다.
“선택형 헤더(UI 토글)에서 setState()로 즉시 반응하는 이미지 교체, Stack/Positioned로 오버레이 배지 구현, CupertinoAlertDialog를 Route로 관리하여 push/pop 기반 팝업 라이프사이클을 설계했습니다.”
Share article

Gyeongwon's blog