CoUI Flutter Quick Reference#
ํ๋ง๋๋ก (๋น๊ฐ๋ฐ์์ฉ)#
์ด ๋ฌธ์๋ ์ฐ๋ฆฌ ์ฑ ํ๋ฉด์ ๋ง๋ค ๋ ์ฐ๋ "๊ณต์ ๋ถํ ์นดํ๋ก๊ทธ"์ด์ "์กฐ๋ฆฝ ์ค๋ช ์"์ ๋๋ค. ๊ฐ๊ตฌ๋ฅผ ๋ง๋ค ๋ ์ ํด์ง ๊ท๊ฒฉ์ ๋์ฌ์ ํ์๋ง ์ฐ๋ฉด ํ๋ค๋ฆผ ์์ด ํผํผํ๋ฏ์ด, ๊ธ์ ํฌ๊ธฐยท๋ฒํผยท๋ ์ง ์ ํ๊ธฐ ๊ฐ์ ํ๋ฉด ๋ถํ(CoUI)์ ์ ํด์ง ๋ฐฉ์๋๋ก๋ง ์ฐ๋๋ก ์ ๋ฆฌํด ๋์์ต๋๋ค. ๋๋ถ์ ๋๊ฐ ๋ง๋ค์ด๋ ์ฑ์ ๋ชจ์๊ณผ ๋๋์ด ์ผ๊ด๋๊ฒ ์ ์ง๋ฉ๋๋ค.
๋ฌด์์ยท์ธ์ (๋น๊ฐ๋ฐ์์ฉ)#
- ๋ฌด์์ ํด ์ฃผ๋์: ๊ธ์ ์คํ์ผ, ๋ฒํผ ํฌ๊ธฐ, ์ ๋ ฅ์นธ, ๋ฐฐ์ง(์ํ ํ์), ๋ ์ง ์ ํ๊ธฐ, ๊ฐ๊ฒฉ, ์์ ๋ฑ ์์ฃผ ์ฐ๋ ํ๋ฉด ๋ถํ์ "์ฌ๋ฐ๋ฅธ ์ฌ์ฉ๋ฒ"๊ณผ "์ฐ๋ฉด ์ ๋๋ ์๋ ๋ฐฉ์"์ ํ๋์ ์ ๋ฆฌํด ์ค๋๋ค.
- ์ธ์ ์ฐ์ด๋์: ๊ฐ๋ฐ์๋ AI๊ฐ ์ ํ๋ฉด์ ๋ง๋ค๊ฑฐ๋ ๊ธฐ์กด ํ๋ฉด์ ์๋ณผ ๋, ์ด ์นดํ๋ก๊ทธ๋ฅผ ํผ์ณ ๋ณด๊ณ ๊ท๊ฒฉ์ ๋ง๋ ๋ถํ์ ๊ณจ๋ผ ์๋๋ค.
- ์ ์ข๋์: ํ๋ฉด๋ง๋ค ์ ๊ฐ๊ฐ์ด ๋๊ฑฐ๋, ๋๋ ์ฐ์ง ์๋ ์๋ ๋ฐฉ์์ ์๋ชป ๊ฐ์ ธ๋ค ์ฐ๋ ์ค์๋ฅผ ์ค์ฌ ์ค๋๋ค. ๊ฒฐ๊ณผ์ ์ผ๋ก ํ๋ฉด์ด ์ผ๊ด๋๊ณ ์ ์ง๋ณด์๊ฐ ์ฌ์์ง๋๋ค.
ํต์ฌ ์ฉ์ด (๋น๊ฐ๋ฐ์์ฉ)#
| ์ฉ์ด | ์ฌ์ด ์ค๋ช |
|---|---|
| CoUI | ์ฐ๋ฆฌ ํ์ด ์ฐ๋ ํ๋ฉด ๋ถํ ๋ชจ์์ง(๋์์ธ ์์คํ ). ์ ํด์ง ๊ท๊ฒฉ์ ๋ถํ๋ง ๊ณจ๋ผ ์ฐ๋๋ก ๋ง๋ ๊ฒ |
| Deprecated | "์ด์ ๋ ์ฐ์ง ๋ง์ธ์"๋ก ํ์๋ ์๋ ๋ฐฉ์. ๋น์ฅ์ ๋์ํด๋ ๊ณง ์ฌ๋ผ์ง๋ฏ๋ก ์ ๋ฐฉ์์ผ๋ก ๋ฐ๊ฟ์ผ ํจ |
| Typography(ํ์ดํฌ๊ทธ๋ํผ) | ๊ธ์์ ํฌ๊ธฐยท๊ตต๊ธฐ ๊ฐ์ ๊ธ์จ ์คํ์ผ ๋ชจ์ |
| TextField | ์ฌ์ฉ์๊ฐ ๊ธ์๋ฅผ ์ ๋ ฅํ๋ ์ ๋ ฅ์นธ |
| Button(๋ฒํผ) | ๋๋ฌ์ ๋์์ ์คํํ๋ ๋ฒํผ. ํฌ๊ธฐ์ ๋นฝ๋นฝํจ ์ ๋๋ฅผ ์ ํด์ง ์ต์ ์์ ๊ณ ๋ฆ |
| Badge(๋ฐฐ์ง) | "ํ๋งค์ค", "ํ๋งค ์ค์ง"์ฒ๋ผ ์ํ๋ฅผ ์๊ฒ ํ์ํ๋ ๋ผ๋ฒจ |
| DatePicker | ๋ฌ๋ ฅ์์ ๋ ์ง๋ฅผ ๊ณ ๋ฅด๋ ๋ถํ |
| FormField / Validator | ์ ๋ ฅ๊ฐ์ด ์ฌ๋ฐ๋ฅธ์ง(์: ํ์ ํญ๋ชฉ ๋๋ฝ) ๊ฒ์ฌํด ์ฃผ๋ ๊ธฐ๋ฅ |
| Skeleton(์ค์ผ๋ ํค) | ๋ด์ฉ์ ๋ถ๋ฌ์ค๋ ๋์ ๋ณด์ฌ ์ฃผ๋ ํ์ ๋น ํ๋ฉด(๋ก๋ฉ ์๋ฆฌํ์) |
| Tab(ํญ) | ํ๋ฉด ์๋จ์์ ํญ๋ชฉ์ ์ ํํ๋ ๊ฐ๋(์: ์ถํ์ฌ / ์ ์ / ๊ณ์ฝ) |
| CoResizable | ํ๋ฉด์ ์ข์ฐ(๋๋ ์ํ)๋ก ๋๋ ๊ฒฝ๊ณ์ ์ ๋์ด ๋น์จ์ ์กฐ์ ํ๋ ๋ถํ ํจ๋ |
| ColorScheme / ์์ ํ ํฐ | ๋ฐฐ๊ฒฝยทํ ๋๋ฆฌยท๊ธ์ ์์ ์ด๋ฆ์ผ๋ก ์ ํด ๋ ํ์ค ์์ํ |
๐ก ์ ์ฒด ๊ฐ์ด๋๋ CLAUDE.md๋ฅผ ์ฐธ์กฐํ์ธ์. ์ด ๋ฌธ์๋ CoUI ์ปดํฌ๋ํธ ์ฌ์ฉ ์ ๋น ๋ฅธ ์ฐธ์กฐ๋ฅผ ์ํ ์์ฝ๋ณธ์ ๋๋ค.
Typography API ๊ฐ์ด๋#
ํ๋ก์ ํธ ํ์ค: context.textStyles + Material 3 Semantic#
์ด ํ๋ก์ ํธ์์๋ context.textStyles + Material 3 semantic styles(titleLarge,
bodyMedium ๋ฑ)๋ฅผ ์ฌ์ฉํฉ๋๋ค (161๊ฑด/30ํ์ผ).
// โ
ํ๋ก์ ํธ ํ์ค: M3 semantic styles ์ฌ์ฉ
style: context.textStyles.titleSmall // 16px + w600
style: context.textStyles.bodyMedium // 16px + w400
style: context.textStyles.labelLarge.copyWith(fontWeight: .w600) // 14px + w600 (์ปค์คํ
)
// โ
copyWith ์กฐํฉ
style: context.textStyles.titleSmall.copyWith(
color: context.appColors.neutral5,
)
API ์ ํ#
| API | ์ํ |
|---|---|
context.textStyles.{titleX/bodyX/labelX/...} |
โ ํ๋ก์ ํธ ํ์ค (M3 semantic) |
context.textStyles.{lgSemibold/sm/xs/...} |
โ ๏ธ Deprecated (Tailwind ์กฐํฉ โ CoUI ์ ๊ท API์์ ์ ๊ฑฐ๋จ) |
context.typography | โ ์ฌ์ฉ ๊ธ์ง |
Material 3 Semantic Styles#
| Getter | Size + Weight | ์ฉ๋ |
|---|---|---|
displayLarge | 52px + w300 | ๋ฉ์ธ ๋์คํ๋ ์ด |
displayMedium | 48px + w300 | ๋์คํ๋ ์ด |
displaySmall | 44px + w300 | ์์ ๋์คํ๋ ์ด |
headlineLarge | 32px + w600 | ํฐ ํค๋๋ผ์ธ |
headlineMedium | 28px + w600 | ํค๋๋ผ์ธ |
headlineSmall | 24px + w600 | ์์ ํค๋๋ผ์ธ |
titleLarge | 20px + w600 | ํ์ด์ง ํ์ดํ |
titleMedium | 18px + w600 | ์น์ ์ ๋ชฉ |
titleSmall | 16px + w600 | ์์ ๋ชฉ, ๊ฐ์กฐ |
bodyLarge | 18px + w400 | ํฐ ๋ณธ๋ฌธ |
bodyMedium | 16px + w400 | ๊ธฐ๋ณธ ๋ณธ๋ฌธ |
bodySmall | 14px + w400 | ์์ ๋ณธ๋ฌธ |
labelLarge | 14px + w400 | ๋ผ๋ฒจ |
labelMedium | 12px + w400 | ์บก์ |
labelSmall | 10px + w400 | ๋ฏธ์ธ ๋ผ๋ฒจ |
Deprecated ๋งคํํ#
| ๊ตฌ๋ฒ์ (Tailwind ์กฐํฉ) | ์ ๋ฒ์ (M3 semantic) | ๋น๊ณ |
|---|---|---|
lgSemibold | titleMedium | 18px + w600 ์ ํ ์ผ์น |
baseSemibold | titleSmall | 16px + w600 ์ ํ ์ผ์น |
smSemibold |
labelLarge.copyWith(fontWeight: .w600) |
weight ์ถ๊ฐ |
baseBold |
titleSmall.copyWith(fontWeight: .w700) |
weight ์ถ๊ฐ |
lg | bodyMedium ๋๋ titleSmall | 16px |
sm | labelMedium | 12px |
xs | labelSmall | 10px |
typography.semiBold |
FontWeight.w600 |
CoUI์์ deprecated |
typography.normal |
FontWeight.w400 |
CoUI์์ deprecated |
TextField features ํ๋ผ๋ฏธํฐ#
โ ๏ธ deprecated:
leading,trailingํ๋ผ๋ฏธํฐ๋ deprecated๋์์ต๋๋ค.
// โ
์๋ก์ด API: features ํ๋ผ๋ฏธํฐ
TextField(
controller: controller,
placeholder: const Text('์
๋ ฅํด์ฃผ์ธ์'),
filled: true, // ๋ฐฐ๊ฒฝ์ ์ ์ฉ (surfaceContainer)
features: [
InputFeature.leading(const Icon(Icons.search, size: 20)),
InputFeature.trailing(const Icon(Icons.clear, size: 18)),
],
onChanged: (value) => ...,
)
// โ deprecated: leading/trailing ํ๋ผ๋ฏธํฐ
TextField(
leading: Icon(Icons.search), // deprecated
trailing: Icon(Icons.clear), // deprecated
)
TextField ์ฃผ์ ์์ฑ#
| ์์ฑ | ํ์ | ์ค๋ช |
|---|---|---|
filled |
bool | true๋ฉด ๋ฐฐ๊ฒฝ์(surfaceContainer) ์ ์ฉ |
border |
Border | ํ
๋๋ฆฌ ์ค์ (Border.all(color: context.colorScheme.surfaceContainerHigh)) |
borderRadius | BorderRadius | ํ ๋๋ฆฌ ๋ฐ๊ฒฝ |
features |
List |
leading, trailing, clear ๋ฑ |
ButtonSize & ButtonDensity#
ButtonSize ์ต์ #
โ ๏ธ ์ฃผ์:
ButtonSize.medium์ ์กด์ฌํ์ง ์์ต๋๋ค!
| ์์ | ์ค์ผ์ผ | ์ฉ๋ |
|---|---|---|
ButtonSize.xSmall | 0.5x | ๋งค์ฐ ์์ ๋ฒํผ |
ButtonSize.small | 0.75x | ์์ ๋ฒํผ |
ButtonSize.normal | 1.0x | ๊ธฐ๋ณธ๊ฐ |
ButtonSize.large | 2.0x | ํฐ ๋ฒํผ |
ButtonSize.xLarge | 3.0x | ๋งค์ฐ ํฐ ๋ฒํผ |
// โ
CORRECT: ์กด์ฌํ๋ ํฌ๊ธฐ ์ฌ์ฉ
ButtonStyle.primary(size: .normal)
ButtonStyle.primary(size: .small)
// โ WRONG: medium ์์ (์ปดํ์ผ ์๋ฌ)
ButtonStyle.primary(size: .medium) // ์๋ฌ!
ButtonDensity ์ต์ #
| ์์ | ์ฉ๋ |
|---|---|
ButtonDensity.normal | ๊ธฐ๋ณธ ๋ฐ๋ |
ButtonDensity.comfortable | ์ฌ์ ๋ก์ด ํจ๋ฉ |
ButtonDensity.dense | ์ปดํฉํธํ ๋ฒํผ (ํ ์ด๋ธ ๋ด ๋ฒํผ) |
ButtonDensity.compact | ์ต์ ํจ๋ฉ |
ButtonDensity.icon | ์์ด์ฝ ์ ์ฉ |
ButtonDensity.iconComfortable | ์์ด์ฝ + ์ฌ์ ํจ๋ฉ |
ButtonDensity.iconDense | ์์ด์ฝ + ์ปดํฉํธ |
// ํ
์ด๋ธ ๋ด ์ปดํฉํธํ ๋ฒํผ
CoButton(
variant: CoreButtonVariant.outline,
size: CoreComponentSize.sm,
onPressed: handleTap,
child: const Text('๋ณต์ฌ'),
)
Badge ์ปดํฌ๋ํธ#
์ํ ํ์์ฉ ๋ฐฐ์ง ์ปดํฌ๋ํธ์
๋๋ค. CoBadge(variant:)๋ก ์ข
๋ฅ๋ฅผ, size:๋ก ํฌ๊ธฐ๋ฅผ ์ ํฉ๋๋ค.
Badge variant#
| variant | ์ฉ๋ | ์์ |
|---|---|---|
CoreBadgeVariant.primary | ๊ธ์ ์ ์ํ (ํ๋งค์ค, ํ์ฑ ๋ฑ) | primary |
CoreBadgeVariant.destructive |
๋ถ์ ์ ์ํ (์ค์ง, ์ญ์ ๋ฑ) | destructive (red) |
์ฌ์ฉ ์์#
// ๊ธฐ๋ณธ ์ฌ์ฉ
const CoBadge(variant: CoreBadgeVariant.primary, child: Text('ํ๋งค์ค'))
const CoBadge(variant: CoreBadgeVariant.destructive, child: Text('ํ๋งค ์ค์ง'))
// size๋ก ์ปดํฉํธํ๊ฒ
const CoBadge(
variant: CoreBadgeVariant.primary,
size: CoreComponentSize.sm,
child: Text('ํ๋งค์ค'),
)
const CoBadge(
variant: CoreBadgeVariant.destructive,
size: CoreComponentSize.sm,
child: Text('ํ๋งค ์ค์ง'),
)
// ์กฐ๊ฑด๋ถ ๋ฐฐ์ง
child: book.isActive
? const CoBadge(
variant: CoreBadgeVariant.primary,
size: CoreComponentSize.sm,
child: Text('ํ๋งค์ค'),
)
: const CoBadge(
variant: CoreBadgeVariant.destructive,
size: CoreComponentSize.sm,
child: Text('ํ๋งค ์ค์ง'),
),
DatePicker ์ปดํฌ๋ํธ#
CoUI DatePicker๋ Material DatePicker์ ๋ค๋ฅธ API๋ฅผ ์ ๊ณตํฉ๋๋ค.
๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ#
DatePicker(
mode: .dialog, // dialog ๋๋ inline
dialogTitle: Text('๋ ์ง ์ ํ'), // ๋ค์ด์ผ๋ก๊ทธ ์ ๋ชฉ (ํ์ ๊ถ์ฅ)
value: currentValue.value, // ํ์ฌ ์ ํ๋ ๋ ์ง
placeholder: Text(field.hint), // ๋ฏธ์ ํ ์ ํ์ ํ
์คํธ
stateBuilder: (date) { ... }, // ๋ ์ง ์ํ ๊ฒฐ์ ํจ์
onChanged: (selectedDate) { ... }, // ๋ณ๊ฒฝ ์ฝ๋ฐฑ
)
์ฃผ์ ํ๋ผ๋ฏธํฐ#
| ํ๋ผ๋ฏธํฐ | ํ์ | ์ค๋ช |
|---|---|---|
mode |
DatePickerMode |
.dialog (ํ์
), .inline (์ธ๋ผ์ธ) |
dialogTitle | Widget? | ๋ค์ด์ผ๋ก๊ทธ ์๋จ ์ ๋ชฉ |
value | DateTime? | ํ์ฌ ์ ํ๋ ๋ ์ง |
placeholder | Widget? | ๋ฏธ์ ํ ์ ํ์ํ ํ ์คํธ |
stateBuilder |
DateState Function(DateTime) |
๋ ์ง๋ณ ์ํ ๊ฒฐ์ |
onChanged | ValueChanged<DateTime>? | ๋ ์ง ๋ณ๊ฒฝ ์ฝ๋ฐฑ |
stateBuilder๋ก ๋ ์ง ๋ฒ์ ์ ํ#
stateBuilder: (date) {
// 1900๋
์ด์ ๋๋ 2100๋
์ดํ ๋ ์ง ๋นํ์ฑํ
if (date.isBefore(DateTime(1900)) ||
date.isAfter(DateTime(2100, 12, 31))) {
return DateState.disabled;
}
return DateState.enabled;
}
DateState ์ต์ #
| ์ํ | ์ค๋ช |
|---|---|
DateState.enabled | ์ ํ ๊ฐ๋ฅ |
DateState.disabled | ์ ํ ๋ถ๊ฐ (ํ์ ์ฒ๋ฆฌ) |
DateState.selected | ํ์ฌ ์ ํ๋ ๋ ์ง |
FormField + CoUI ์์ ฏ ํตํฉ ํจํด#
CoUI ์์ ฏ์ Material FormField์ ํตํฉํ์ฌ ๊ฒ์ฆ ๊ธฐ๋ฅ์ ์ถ๊ฐํ ์ ์์ต๋๋ค.
DatePicker + FormField ์์#
material.FormField<DateTime>(
builder: (formFieldState) {
return Column(
crossAxisAlignment: .start,
children: [
Expanded(
child: DatePicker(
mode: .dialog,
dialogTitle: Text(context.i10n.common.select_date),
value: currentValue.value,
placeholder: Text(field.hint),
stateBuilder: (date) {
if (date.isBefore(DateTime(1900)) ||
date.isAfter(DateTime(2100, 12, 31))) {
return DateState.disabled;
}
return DateState.enabled;
},
onChanged: (selectedDate) {
dateNotifier.value = selectedDate;
formFieldState.didChange(selectedDate); // โ
ํ์: ๊ฒ์ฆ ๋๊ธฐํ
},
),
),
if (formFieldState.hasError) ...[
const CoGap(size: CoreSpace.space4),
Text(
formFieldState.errorText!,
style: FormInputStyles.errorTextStyle(context.appColors),
),
],
],
);
},
validator: (value) {
if (field.isRequired && value == null) {
return context.i10n.common.date_required(label: field.label);
}
return null;
},
)
ํต์ฌ ํฌ์ธํธ#
| ํญ๋ชฉ | ์ค๋ช |
|---|---|
formFieldState.didChange(value) |
ํ์ - FormField ๊ฒ์ฆ ์ํ ๋๊ธฐํ |
formFieldState.hasError | ์๋ฌ ์กด์ฌ ์ฌ๋ถ ํ์ธ |
formFieldState.errorText | validator ๋ฐํ ์๋ฌ ๋ฉ์์ง |
FormInputStyles.errorTextStyle() | ํ๋ก์ ํธ ํ์ค ์๋ฌ ํ ์คํธ ์คํ์ผ |
๋์ ๋์ด ๊ณ์ฐ (์๋ฌ ์ํ)#
์๋ฌ ๋ฉ์์ง ํ์๋ก ์ธํ ๋์ด ๋ณํ๋ฅผ ์ฒ๋ฆฌํด์ผ ํ ๊ฒฝ์ฐ:
// ์๋ฌ ์ํ์ ๋ฐ๋ฅธ ๋์ด ๊ณ์ฐ
final baseHeight = 40.0;
final errorHeight = formFieldState.hasError ? 20.0 : 0.0;
final totalHeight = baseHeight + errorHeight;
ValueNotifier + useEffect ์ธ๋ถ ๋ณ๊ฒฝ ๊ฐ์ง#
@override
Widget build(BuildContext context) {
final dateNotifier = useState<DateTime?>(initialValue);
// ์ธ๋ถ์์ ๊ฐ์ด ๋ณ๊ฒฝ๋๋ฉด ๋๊ธฐํ
useEffect(() {
if (externalValue != dateNotifier.value) {
dateNotifier.value = externalValue;
}
return null;
}, [externalValue]);
return DatePicker(
value: dateNotifier.value,
onChanged: (date) {
dateNotifier.value = date;
onExternalChange?.call(date);
},
);
}
CoGap & Insets ์์#
CoGap ์ฌ์ฉ๋ฒ#
โ ๏ธ deprecated:
Gap.sX(),Gap(double),gapํจํค์ง๋ ๋ชจ๋ deprecated. ์ ๊ท ์ฝ๋๋ ๋ฐ๋์CoGap์ ์ฌ์ฉํฉ๋๋ค (Epic #5256, #5125).
const CoGap(size: CoreSpace.space4) // 4px
const CoGap(size: CoreSpace.space8) // 8px
const CoGap(size: CoreSpace.space16) // 16px
const CoGap(size: CoreSpace.space24) // 24px
CoGap(size: CoreSpace.space16) // Insets.medium ๋์ฒด
Gap โ CoGap ๋งคํํ#
๊ตฌ๋ฒ์ (Gap) | ์ ๋ฒ์ (CoGap) | ๊ฐ |
|---|---|---|
Gap.s1() | CoGap(size: CoreSpace.space4) | 4px |
Gap.s2() | CoGap(size: CoreSpace.space8) | 8px |
Gap.s3() | CoGap(size: CoreSpace.space12) | 12px |
Gap.s4() | CoGap(size: CoreSpace.space16) | 16px |
Gap.s6() | CoGap(size: CoreSpace.space24) | 24px |
Gap.s8() | CoGap(size: CoreSpace.space32) | 32px |
Gap(Insets.small) |
CoGap(size: CoreSpace.space8) |
8px |
Gap(Insets.medium) |
CoGap(size: CoreSpace.space16) |
16px |
Gap(Insets.large) |
CoGap(size: CoreSpace.space24) |
24px |
Insets ์์ ํ#
์ ๊ท ์ฝ๋๋
Insets.*๋์CoreSpace.spaceN์ ์ฌ์ฉํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค (CoUI ๋์์ธ ํ ํฐ๊ณผ ์ง์ ์ ํฉ).
| Insets | ๊ฐ | CoreSpace ๋งคํ | ์ฉ๋ |
|---|---|---|---|
Insets.xSmall | 4px | CoreSpace.space4 | ์์ด์ฝ ๊ฐ๊ฒฉ |
Insets.small | 8px | CoreSpace.space8 | ์์ ๊ฐ๊ฒฉ |
Insets.medium |
16px | CoreSpace.space16 |
์น์ ํจ๋ฉ |
Insets.large | 24px | CoreSpace.space24 | ์นด๋ ํจ๋ฉ |
Insets.xLarge |
32px | CoreSpace.space32 |
ํ์ด์ง ๋ง์ง |
๋ก๋ฉ/์ค์ผ๋ ํค ํจํด#
// ์ค์ผ๋ ํค ๋ก๋ฉ (์ ์ฒด ์์ ฏ)
widget.skeletonizer(enabled: state.isLoading)
// ์กฐ๊ฑด๋ถ ๋ก๋ฉ (์คํผ๋)
widget.loadingOr(
isLoading: state.isLoading,
loadingWidget: const CircularProgressIndicator(),
)
// ๋น ์ํ ์ฒ๋ฆฌ
listWidget.emptyOrWhen(
condition: () => list.isEmpty,
emptyWidget: const EmptyStateWidget(),
)
useEffect ํญ ๋ฐ์ดํฐ ๋ก๋ฉ ํจํด (HookWidget)#
ํญ ๊ธฐ๋ฐ UI์์ ๊ฐ ํญ์ ์ด๊ธฐ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ ๋ useEffect๋ฅผ ํ์ฉํฉ๋๋ค.
๊ธฐ๋ณธ ํจํด#
class MyTabbedPage extends HookWidget {
const MyTabbedPage({super.key});
@override
Widget build(BuildContext context) {
final selectedTab = useState(_Tab.first);
// โ
ํญ ๋ณ๊ฒฝ ์ ๋ฐ์ดํฐ ๋ก๋
useEffect(() {
final bloc = context.read<MyBloc>();
switch (selectedTab.value) {
case _Tab.first:
bloc.add(const MyEvent.loadFirstTab());
case _Tab.second:
bloc.add(const MyEvent.loadSecondTab());
case _Tab.third:
bloc.add(const MyEvent.loadThirdTab());
}
return null; // cleanup ๋ถํ์
}, [selectedTab.value]); // ์์กด์ฑ: ํญ ๋ณ๊ฒฝ ์๋ง๋ค ์คํ
return TabBarView(
selectedTab: selectedTab,
// ...
);
}
}
// โ
private enum์ผ๋ก ํญ ์ ์ (๋งค์ง ๋๋ฒ ๋ฐฉ์ง)
enum _Tab { first, second, third }
์ฃผ์ ํฌ์ธํธ#
| ํญ๋ชฉ | ์ค๋ช |
|---|---|
useEffect ์์กด์ฑ | [selectedTab.value] - ํญ ๋ณ๊ฒฝ ์๋ง๋ค ์คํ |
| cleanup ๋ฐํ | return null; - ๋ฐ์ดํฐ ๋ก๋๋ cleanup ๋ถํ์ |
| BLoC ์ด๋ฒคํธ | ๊ฐ ํญ๋ณ๋ก ๋ณ๋์ ๋ก๋ ์ด๋ฒคํธ ๋ฐ์ |
| enum ์ฌ์ฉ | private enum์ผ๋ก ํญ ์ธ๋ฑ์ค ๊ด๋ฆฌ (๋งค์ง ๋๋ฒ ๋ฐฉ์ง) |
์ด๊ธฐ ๋ก๋ฉ๊ณผ ํญ ๋ณ๊ฒฝ ๋ก๋ฉ ๋ถ๋ฆฌ#
@override
Widget build(BuildContext context) {
final selectedTab = useState(_Tab.first);
final isInitialized = useState(false);
// ์ด๊ธฐ ๋ก๋ฉ (ํ ๋ฒ๋ง)
useEffect(() {
context.read<MyBloc>().add(const MyEvent.initialize());
isInitialized.value = true;
return null;
}, const []); // ๋น ์์กด์ฑ = ์ต์ด 1ํ
// ํญ ๋ณ๊ฒฝ ์ ๋ก๋ฉ (์ด๊ธฐํ ํ์๋ง)
useEffect(() {
if (!isInitialized.value) return null;
final bloc = context.read<MyBloc>();
switch (selectedTab.value) {
case _Tab.first:
bloc.add(const MyEvent.loadFirstTab());
// ...
}
return null;
}, [selectedTab.value]);
// ...
}
Private Enum ํญ ๊ด๋ฆฌ#
// โ
CORRECT: private enum ์ฌ์ฉ
enum _Tab {
publishers, // ์ถํ์ฌ ํญ
authors, // ์ ์ ํญ
contracts, // ๊ณ์ฝ ํญ
}
// Widget์์ ์ฌ์ฉ
final selectedTab = useState(_Tab.publishers);
// Switch expression์ผ๋ก ํญ๋ณ ๋ก์ง
switch (selectedTab.value) {
case _Tab.publishers => publishersList,
case _Tab.authors => authorsList,
case _Tab.contracts => contractsList,
}
// โ WRONG: ๋งค์ง ๋๋ฒ ์ฌ์ฉ
final selectedTab = useState(0); // 0์ด ๋ญ์ง?
if (selectedTab.value == 0) { ... } // ๊ฐ๋
์ฑ ๋์จ
์์ ํ ํ์ด์ง ์์#
class MyPage extends HookWidget {
const MyPage({super.key});
@override
Widget build(BuildContext context) {
final appColors = context.appColors;
return BlocProvider(
create: (_) => getIt<MyBloc>(),
child: BlocBuilder<MyBloc, MyState>(
builder: (context, state) {
return Scaffold(
headers: [
AppBar(
title: Text(
'ํ์ด์ง ์ ๋ชฉ',
style: context.textStyles.titleMedium.copyWith(
color: appColors.neutral5,
),
),
),
],
child: Padding(
padding: const .symmetric(horizontal: Insets.medium),
child: Column(
crossAxisAlignment: .start,
children: [
const CoGap(size: CoreSpace.space16),
TextField(
placeholder: const Text('์ด๋ฆ์ ์
๋ ฅํ์ธ์'),
features: const [
InputFeature.leading(Icon(Icons.person, size: 20)),
],
onChanged: (value) => context.read<MyBloc>().add(
MyEvent.nameChanged(value),
),
),
const CoGap(size: CoreSpace.space16),
CoButton(
variant: CoreButtonVariant.primary,
expanded: true,
enabled: state.isValid,
onPressed: () => context.read<MyBloc>().add(
const MyEvent.submitted(),
),
child: const Text('์ ์ฅ'),
),
],
),
).skeletonizer(enabled: state.isLoading),
);
},
),
);
}
}
CoResizable ์ข์ฐ ๋ถํ ํจ๋#
CoResizable ์ ์ข์ฐ(๋๋ ์ํ) ๋ถํ ํจ๋์ ๋๋๊ทธ๋ก ๋น์จ ์กฐ์ ํ ์ ์๋ ์ปดํฌ๋ํธ์
๋๋ค. ๋ทฐ์ด ์ฌ์ด๋ ํจ๋ ๋ฑ ๋ฐ์ํ ๋ ์ด์์์ ํ์ฉ (Epic #5883,
.claude/rules/viewer-side-panels.md ์ฐธ์กฐ).
CoResizable(
// direction: .horizontal (๊ธฐ๋ณธ๊ฐ), ์ธ๋ก ๋ถํ ์ด๋ฉด .vertical
panes: [
CoreResizablePaneData(
initialSize: 0.65, // ์ด๊ธฐ ๋น์จ (ํฉ์ด 1.0)
minSize: 0.4, // ์ต์ ๋น์จ (์ ํ)
// maxSize: 0.8, // ์ต๋ ๋น์จ (์ ํ)
content: leftWidget,
),
CoreResizablePaneData(
initialSize: 0.35,
minSize: 0.25,
content: rightWidget,
),
],
)
๋ฐ์ํ ํจํด (๋ทฐ์ด ์ฌ์ด๋ ํจ๋)#
LayoutBuilder + breakpoint ๋ถ๊ธฐ๋ก ์ข์ ํ๋ฉด์์๋ ๋จ์ผ ํจ๋, ๋์ ํ๋ฉด์์๋ split ์ ์ฉ:
LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth >= 768;
if (!isWide || !isPanelOpen) return viewer;
return CoResizable(
panes: [
CoreResizablePaneData(
initialSize: 0.65,
minSize: 0.4,
content: viewer,
),
CoreResizablePaneData(
initialSize: 0.35,
minSize: 0.25,
content: panelHost,
),
],
);
},
)
์ฃผ์#
-
panes์initialSizeํฉ์1.0์ด ๋์ด์ผ ๊ท ํ. CoUI ๊ฐflexPrecision์ผ๋ก ์ ์ flex ๋ณํ. CoresizableDirection์.horizontal(๊ธฐ๋ณธ) ๋๋.vertical.- ํจ๋ ์ฝํ
์ธ ๋ ์์ฒด layout ์ ์ฑ
์ ๋ฐ๋ฆ โ
Expanded/SafeArea๋ฑ์ ์ฝํ ์ธ ์์ ฏ์ด ์ง์ ๊ด๋ฆฌ.
์์ ์ฒด๊ณ (Console UI)#
AppColors neutral ์๋ฆฌ์ฆ#
| ์์ | HEX | ์ฉ๋ |
|---|---|---|
neutral100 | #FFFFFF | ํฐ์ ๋ฐฐ๊ฒฝ |
neutral95 | #F9FAFB | ํค๋/์น์ ๋ฐฐ๊ฒฝ (๋ณธ๋ฌธ๊ณผ ๊ตฌ๋ถ) |
neutral85 | #F4F5F6 | ๋ฐ์ ํ์ |
neutral80 | #ECEEEF | ๊ธฐ๋ณธ ํ ๋๋ฆฌ |
neutral70 | #B4B8C4 | ์งํ ํ ๋๋ฆฌ (์ ๋ช ํ ๊ตฌ๋ถ์ ) |
neutral60 | #8F929A | ๋นํ์ฑ ํ ์คํธ |
neutral30 | - | ๋ณธ๋ฌธ ํ ์คํธ |
// ํค๋ ๋ฐฐ๊ฒฝ (๋ณธ๋ฌธ๊ณผ ๊ตฌ๋ถ)
color: context.appColors.neutral95,
// ์งํ ํ
๋๋ฆฌ (์ ๋ช
ํ๊ฒ)
border: Border(
right: .new(color: context.appColors.neutral70),
),
// ๊ธฐ๋ณธ ํ
๋๋ฆฌ
border: Border.all(color: context.appColors.neutral80),
ColorScheme M3 ํ ํฐ (Material 3 ํ์ค)#
CoUI ์ ๊ท API๋ Material 3 ColorScheme ํ ํฐ์ ํ์ค์ผ๋ก ์ฌ์ฉํฉ๋๋ค.
| ์์ | ์ฉ๋ |
|---|---|
surface | ๊ฐ์ฅ ๋ฐ์ ๋ฐฐ๊ฒฝ |
surfaceContainer | ์ค๊ฐ ๋ฐฐ๊ฒฝ (filled TextField) |
surfaceContainerHigh | ๊ฐ์ฅ ์งํ ๋ฐฐ๊ฒฝ/ํ ๋๋ฆฌ |
onSurface | ํ๋ฉด ์ ํ ์คํธ |
onSurfaceVariant | ๋ณด์กฐ ํ ์คํธ (๋ฐํฌ๋ช ) |
outlineVariant | ์ฝํ ํ ๋๋ฆฌ/๊ตฌ๋ถ์ |
โ ๏ธ Deprecated:
base100/200/300,baseContent๋ CoUI ์ ๊ท API์์ deprecated. M3 ํ ํฐ ์ฌ์ฉ ๊ถ์ฅ.
// TextField ๋ฐฐ๊ฒฝ
TextField(
filled: true, // surfaceContainer ๋ฐฐ๊ฒฝ
border: Border.all(color: context.colorScheme.surfaceContainerHigh),
)
// ํํฐ ๋ค์ด์ผ๋ก๊ทธ ํ
๋๋ฆฌ
OutlinedContainer(
backgroundColor: theme.colorScheme.surface,
borderColor: theme.colorScheme.surfaceContainerHigh, // ์งํ ํ
๋๋ฆฌ
)
์ฐธ์กฐ ๋ฌธ์#
๋ค์ ๋ด์ฉ์ **CLAUDE.md**๋ฅผ ์ฐธ์กฐํ์ธ์:
- ์์ ๊ท์น (AppColors, context.appColors)
- Dot Shorthand ๊ท์น (Dart 3.10+)
- Button/IconButton ์ฌ์ฉ๋ฒ
- BLoC Event/State ํจํด (sealed class)
- BLoC async ํธ๋ค๋ฌ (isClosed ์ฒดํฌ)
- Context Extensions ์ ์ฒด ๋ชฉ๋ก