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),
),
),
),
],
),
);
}
}

- 선이 있는 컨테이너 위로(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),
),
],
),
),

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),
),
],
),
),

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),
),
],
),
),

상태 변경 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