ResponsiveRowColumn#
ResponsiveRowColumn은 children을 switchAt tier 이상에서는 Row(가로), 미만에서는 Column(세로)
로 렌더링하는 레이아웃 primitive입니다. responsive_framework의 동명 ResponsiveRowColumn을 CoUI 크로스플랫폼(Flutter+Web) 버전으로 옮긴 것입니다.
방향(direction)은 위젯 파라미터가 아니라 매 build마다 앰비언트 viewport breakpoint tier에서 파생됩니다 — Breakpoint/resolveBreakpoint
공유 접근자(Breakpoint 참조)를 그대로 재사용하며, 이 컴포넌트 자신은 별도의 측정(LayoutBuilder/MediaQuery
직접 호출)을 하지 않습니다.
Live Preview#
class ResponsiveRowColumnDefaultExample extends StatelessComponent {
const ResponsiveRowColumnDefaultExample({super.key});
@override
Component build(BuildContext context) {
return ResponsiveRowColumn(
responsiveRowColumnStyle: const CoreResponsiveRowColumnStyle(
spacing: CoreSpace.space12,
),
children: [
_StatCard(label: 'Views', value: '1,204'),
_StatCard(label: 'Likes', value: '86'),
_StatCard(label: 'Comments', value: '12'),
],
);
}
}
class _StatCard extends StatelessComponent {
const _StatCard({required this.label, required this.value});
final String label;
final String value;
@override
Component build(BuildContext context) {
return Card(
cardStyle: const CoreCardStyle(
padding: CoreEdgeInsets.all(CoreSpace.space16),
),
child: div(
classes: 'flex flex-col items-start',
[
Text(value).titleMedium.semiBold.onSurface,
Text(label).bodyMedium.onSurfaceVariant,
],
),
);
}
}
class ResponsiveRowColumnDefaultExample extends StatelessWidget {
const ResponsiveRowColumnDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return ResponsiveRowColumn(
responsiveRowColumnStyle: const CoreResponsiveRowColumnStyle(
spacing: CoreSpace.space12,
),
children: [
_StatCard(label: 'Views', value: '1,204'),
_StatCard(label: 'Likes', value: '86'),
_StatCard(label: 'Comments', value: '12'),
],
);
}
}
class _StatCard extends StatelessWidget {
const _StatCard({required this.label, required this.value});
final String label;
final String value;
@override
Widget build(BuildContext context) {
return Card(
cardStyle: const CoreCardStyle(
padding: CoreEdgeInsets.all(CoreSpace.space16),
),
child: Column(
mainAxisSize: .min,
crossAxisAlignment: .start,
children: [
Text(value).titleMedium.semiBold.onSurface,
Text(label).bodyMedium.onSurfaceVariant,
],
),
);
}
}
class ResponsiveRowColumnDesktopExample extends StatelessComponent {
const ResponsiveRowColumnDesktopExample({super.key});
@override
Component build(BuildContext context) {
return ResponsiveRowColumn(
switchAt: .desktop,
responsiveRowColumnStyle: const CoreResponsiveRowColumnStyle(
spacing: CoreSpace.space12,
),
children: [
_StatCard(label: 'Views', value: '1,204'),
_StatCard(label: 'Likes', value: '86'),
_StatCard(label: 'Comments', value: '12'),
],
);
}
}
class _StatCard extends StatelessComponent {
const _StatCard({required this.label, required this.value});
final String label;
final String value;
@override
Component build(BuildContext context) {
return Card(
cardStyle: const CoreCardStyle(
padding: CoreEdgeInsets.all(CoreSpace.space16),
),
child: div(
classes: 'flex flex-col items-start',
[
Text(value).titleMedium.semiBold.onSurface,
Text(label).bodyMedium.onSurfaceVariant,
],
),
);
}
}
class ResponsiveRowColumnDesktopExample extends StatelessWidget {
const ResponsiveRowColumnDesktopExample({super.key});
@override
Widget build(BuildContext context) {
return ResponsiveRowColumn(
switchAt: .desktop,
responsiveRowColumnStyle: const CoreResponsiveRowColumnStyle(
spacing: CoreSpace.space12,
),
children: [
_StatCard(label: 'Views', value: '1,204'),
_StatCard(label: 'Likes', value: '86'),
_StatCard(label: 'Comments', value: '12'),
],
);
}
}
class _StatCard extends StatelessWidget {
const _StatCard({required this.label, required this.value});
final String label;
final String value;
@override
Widget build(BuildContext context) {
return Card(
cardStyle: const CoreCardStyle(
padding: CoreEdgeInsets.all(CoreSpace.space16),
),
child: Column(
mainAxisSize: .min,
crossAxisAlignment: .start,
children: [
Text(value).titleMedium.semiBold.onSurface,
Text(label).bodyMedium.onSurfaceVariant,
],
),
);
}
}
class ResponsiveRowColumnChainExample extends StatelessComponent {
const ResponsiveRowColumnChainExample({super.key});
@override
Component build(BuildContext context) {
return ResponsiveRowColumn(
children: [
_StatCard(label: 'Views', value: '1,204'),
_StatCard(label: 'Likes', value: '86'),
_StatCard(label: 'Comments', value: '12'),
],
).withStyle(
const CoreResponsiveRowColumnStyle(
spacing: CoreSpace.space24,
mainAxisAlignment: CoreMainAxisAlignment.center,
crossAxisAlignment: CoreCrossAxisAlignment.start,
),
);
}
}
class _StatCard extends StatelessComponent {
const _StatCard({required this.label, required this.value});
final String label;
final String value;
@override
Component build(BuildContext context) {
return Card(
cardStyle: const CoreCardStyle(
padding: CoreEdgeInsets.all(CoreSpace.space16),
),
child: div(
classes: 'flex flex-col items-start',
[
Text(value).titleMedium.semiBold.onSurface,
Text(label).bodyMedium.onSurfaceVariant,
],
),
);
}
}
class ResponsiveRowColumnChainExample extends StatelessWidget {
const ResponsiveRowColumnChainExample({super.key});
@override
Widget build(BuildContext context) {
return ResponsiveRowColumn(
children: [
_StatCard(label: 'Views', value: '1,204'),
_StatCard(label: 'Likes', value: '86'),
_StatCard(label: 'Comments', value: '12'),
],
).withStyle(
const CoreResponsiveRowColumnStyle(
spacing: CoreSpace.space24,
mainAxisAlignment: CoreMainAxisAlignment.center,
crossAxisAlignment: CoreCrossAxisAlignment.start,
),
);
}
}
class _StatCard extends StatelessWidget {
const _StatCard({required this.label, required this.value});
final String label;
final String value;
@override
Widget build(BuildContext context) {
return Card(
cardStyle: const CoreCardStyle(
padding: CoreEdgeInsets.all(CoreSpace.space16),
),
child: Column(
mainAxisSize: .min,
crossAxisAlignment: .start,
children: [
Text(value).titleMedium.semiBold.onSurface,
Text(label).bodyMedium.onSurfaceVariant,
],
),
);
}
}
두 프리뷰가 다른 방향(Row/Column)으로 보일 수 있습니다 — 전환 기준이 각 데모 자신의 뷰포트이기 때문입니다. Web 데모는 브라우저 창 너비를, Flutter 데모는 페이지에 임베드된 Flutter 뷰의 너비를 기준으로 전환합니다(Breakpoint 와 동일한 시맨틱). 실제 Flutter 앱에서는 OS 창 너비가 기준입니다.
브라우저 창(또는 에뮬레이터)을 리사이즈하면 Row↔Column 전환을 확인할 수 있습니다. default variant는
switchAt: tablet(기본값)이라 tablet tier 이상에서 가로로 배치되고, desktop variant는
switchAt: .desktop으로 임계값을 높여 desktop tier 미만에서는 계속 세로로 쌓입니다.
사용 시기#
- 카드/통계/폼 필드 묶음을 넓은 화면에서는 가로로, 좁은 화면에서는 세로로 쌓고 싶을 때
responsive_framework의ResponsiveRowColumn을 CoUI 네이티브 컴포넌트로 교체할 때-
이미 다른 곳에서 소비 중인
CoreBreakpointTier(3단계: mobile/tablet/desktop)와 같은 판정 기준을 공유하고 싶을 때 —Breakpoint가 쓰는 것과 동일한resolveBreakpoint진입점을 사용합니다
값에 따라 자식을 통째로 다른 위젯으로 교체하고 싶다면 대신 MediaQuery(MediaQueryVisibility)를 사용하세요.
기본 사용법#
ResponsiveRowColumn(
responsiveRowColumnStyle: const CoreResponsiveRowColumnStyle(
spacing: CoreSpace.space12,
),
children: [
_StatCard(label: 'Views', value: '1,204'),
_StatCard(label: 'Likes', value: '86'),
_StatCard(label: 'Comments', value: '12'),
],
)
ResponsiveRowColumn(
responsiveRowColumnStyle: const CoreResponsiveRowColumnStyle(
spacing: CoreSpace.space12,
),
children: [
_StatCard(label: 'Views', value: '1,204'),
_StatCard(label: 'Likes', value: '86'),
_StatCard(label: 'Comments', value: '12'),
],
)
switchAt — tier 임계값 override#
ResponsiveRowColumn(
switchAt: .desktop, // 기본값 .tablet 대신 .desktop 이상에서만 Row
children: [...],
)
switchAt은 BEHAVIOUR 값(어느 tier에서 어느 axis로 렌더할지를 결정)이라 시각 chrome이 아니므로, CoreXxxStyle
슬롯이 아니라 CoreResponsiveRowColumnContract.defaultSwitchAt(기본값 .tablet) 위에 직접 얹힙니다 —
Breakpoint의 resolveBreakpoint가 boundaries를 다루는 것과 같은 위치 원칙입니다.
Props#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
children |
List<Widget> / List<Component> |
required | 렌더할 자식 목록 |
switchAt |
CoreBreakpointTier? |
CoreBreakpointTier.tablet |
이 tier 이상에서 Row, 미만에서 Column |
responsiveRowColumnStyle |
CoreResponsiveRowColumnStyle? |
— | 간격/정렬 Style 슬롯 (아래 표) |
CoreResponsiveRowColumnStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
spacing |
double? |
Evenly-distributed spacing between children (logical px at 1.0 scaling). This is N-sibling spacing — it flows through the platform-native
Row
/
Column
/
Flex.spacing
API (Flutter) or a
gap-*
utility (Web), not a 1-off
CoreGapStyle
slot (
resolver/scaling-and-gap.md
).
|
mainAxisAlignment |
CoreMainAxisAlignment? |
Main-axis alignment — see the class doc for the direction-relative meaning. |
crossAxisAlignment |
CoreCrossAxisAlignment? |
Cross-axis alignment — see the class doc for the direction-relative meaning. |
동작#
-
tier ≥
switchAt:Axis.horizontal— 자식이Row처럼 가로로 배치 -
tier <
switchAt:Axis.vertical— 자식이Column처럼 세로로 스택 -
tier 판정은 Breakpoint와 동일한
resolveBreakpoint(context)호출 결과(CoreBreakpointTierOrder.isAtLeast)를 그대로 재사용합니다 — 별도의 breakpoint 정의나 측정 로직이 없습니다
플랫폼 차이#
-
Flutter: 단일
Flex(direction:, mainAxisAlignment:, crossAxisAlignment:, spacing:)—direction만 tier에 따라Axis.horizontal/Axis.vertical로 선택되고 나머지는 그대로 네이티브FlexAPI로 전달 -
Web: 단일
<div>—flex flex-row/flex flex-colTailwind 클래스 +justify-*/items-*정렬 클래스 + inline-CSSgap. 브라우저가 CSS 변수로 spacing을 런타임 해석하므로 텍스트 스케일에 자동 반응
두 플랫폼 모두 같은 CoreBreakpointTier 판정을 공유하므로 전환 시점이 픽셀 단위로 일치합니다.
사용 가이드라인 (Usage Guidelines)#
✅ Do#
간격은 수동 Gap/SizedBox 대신 spacing으로
ResponsiveRowColumn(
responsiveRowColumnStyle: const CoreResponsiveRowColumnStyle(
spacing: CoreSpace.space12,
),
children: [statCardA, statCardB, statCardC],
)
spacing은 Row/Column 어느 쪽으로 렌더되든 네이티브 Flex.spacing(Flutter) / CSS gap(Web)으로 그대로 흐르므로, 방향이 바뀌어도 간격을 다시 계산할 필요가 없습니다.
❌ Don't#
mainAxisAlignment/crossAxisAlignment이 고정된 화면 방향을 뜻한다고 가정하지 않기
// ❌ "항상 세로 중앙 정렬"을 기대하면 안 됨
ResponsiveRowColumn(
responsiveRowColumnStyle: const CoreResponsiveRowColumnStyle(
crossAxisAlignment: CoreCrossAxisAlignment.center,
),
children: [...],
)
두 정렬 필드는 direction-relative입니다 — tier가 바뀌어 Row↔Column으로 전환되면 같은 crossAxisAlignment: .center도 어떤 tier에서는 세로 중앙, 다른 tier에서는 가로 중앙을 의미하게 됩니다. "항상 왼쪽 정렬" 같은 절대 방향을 기대하지 마세요.
접근성 (Accessibility)#
역할 / Semantics#
양 플랫폼 모두 아무 역할도 내보내지 않습니다. Flutter는 순수한 Flex를 반환하고 디렉터리 어디에도 Semantics가 없으며, Web은 flex/gap 클래스만 붙은 평범한
<div>를 렌더하고 호출자가 넘긴 attributes만 통과시킵니다.
읽는 입장에서 이것은 자식을 그대로 통과시키는 레이아웃 래퍼입니다. 이 묶음이 하나의 그룹으로 읽혀야 한다면 그룹 역할과 이름은 호출자가 붙여야 합니다.
키보드#
처리하는 키가 없습니다. Web 생성자는 attributes / classes / css
/ eventHandlers / id / key만 받으므로 키보드 핸들러 패스스루 prop조차 없습니다 — 키 처리가 필요하면
eventHandlers로 직접 넣어야 합니다.
포커스#
포커스를 받지 않습니다. 양 플랫폼 모두 FocusNode/Focus/tabindex가 없어 탭 순서에 끼어들지 않고, 자식은 각자 가져온 포커스 동작을 그대로 유지합니다.
스크린 리더#
스스로는 아무것도 읽히지 않고, 읽히는 것은 자식의 내용뿐입니다. 축이 가로에서 세로로 바뀌어도 자식은 선언 순서 그대로 읽히며, 축이 바뀌었다는 사실 자체는 어디에도 알려지지 않습니다.
알려진 제약#
- 순수한 레이아웃 스위치입니다. 브레이크포인트 tier에 따라 row/column을 고르는 것 외에 아무것도 하지 않습니다 — 자식의 논리적 순서를 재배치하지 않고, 자식을 시맨틱하게 묶지도 않습니다. 시각 순서와 읽기 순서가 달라야 하는 배치라면 그 조정은 호출자 몫입니다.
-
Flutter에는
attributes대응 패스스루가 아예 없어 호출자가 바깥에서 직접 감싸지 않으면 시맨틱을 주입할 수 없습니다.
전역으로 보장되는 항목(동작 줄이기·고대비·색 강제 모드 등)은 전역 접근성 축을 참고하세요.
관련 컴포넌트#
-
Breakpoint —
ResponsiveRowColumn이 방향을 파생시키는 데 재사용하는 공유 tier 접근자 - ResponsiveContainer — 단일 콘텐츠에 max-width 제약을 주는 래퍼
- ResponsiveGrid — N×M 셀 그리드가 필요할 때