위젯의 크기가 어떻게 결정되는지 이해하기 위해서는 레이아웃과 제약 조건개념에 대해 알아둘 필요가 있다.
플러터는 결국 UI 라이브러리이며 렌더링 엔진이기 때문인데, 이를 이해하지 못 할수록 flutter layout infinite size(플러터 레이아웃 무한 크기)오류를 겪게 될 것이다.
이러한 에러를 고치기 위해서는 플러터가 화면의 픽셀을 어떻게 칠하고 제약 조건의 역할은 무엇인지 알 필요가 있다.
위에서 언급한 flutter layout infinite size 오류는 위젯이 수평, 수직으로 무한 크기를 갖도록 제약조건이 설정되었을 때 발생한다. 즉, 이 오류는 렌더 객체가 전달된 제약 조건을 처리하는 과정에서 발생한다.
레이아웃 위젯중 Row, Column 처럼 스크롤할 수 있는 위젯에서 길이(너비)는 이론적으로는 무한할 수 있다. 하지만 컴퓨터의 연산능력, 시간의 제약 등등 때문에 정말 무한해질수는 없다.
Row, Column 레이아웃 위젯은 플렉스 상자로 이들의 렌더 객체는 위에 설명한 세 가지 렌더 객체 동작 유형에 속하지 않고, 부모가 전달한 제약 조건에 따라 다른 동작을 수행한다.
그렇기에 이들은 부모가 한정된 제약 조건을 가지면 그 한정된 제약 조건 내에서 최대한의 공간을 차지한다.
그렇기에 이미지가 여러개인 Column이라면 가장 높아가 큰 이미지의 높이가 Column의 높이가 된다. 그럼 생각해 볼 이슈가 있다. Column 위젯은 자식이 원하는 크기를 갖도록 하는데, 자식이 제약없이 허용된 최대 크기를 선택한다면 오류가 발생한다.
child: Column(children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: Text("..."),
)
])
]),
위 코드에서 Column은 자식이 원하는 크기만큼 갖도록 하며, 자식에서는 Expanded로 부모가 허락하는 만큼 최대한이라는 제약조건이 두개가 겹치며 오류를 발생시킨다.
위젯은 위처럼 제약 조건을 트리 아래로 전달하기에 중복된 플렉스 상자를 어느정도 분리해야 한다. 개발을 하다보면 Column안에 Row를 갖거나 반대의 경우와 같은 플렉시블 위젯을 중첩해서 사용하는 경우가 잦다.
우리는 플렉시블 위젯 동작을 최대한 이해해 이 문제를 쉽게 해결할 수 있도록 해야 한다.
Center 안에 넣은 위젯의 widget:100이 왜 100px이 아닌지, FittedBox가 가끔 오작동하는지, Column이 넘쳐흐르는지(overflowing), 혹은 IntrinsicWidth 는 어떻게 작동해야 하는 지...
Flutter의 레이아웃은 규칙을 모르면 이해할 수 없으므로 모두 모두 일찍 배워 놓도록 하자!
👉 Constraints go down. Sizes go up. Positions are set by parents.
제약이 줄어든다. 사이즈는 올라간다. 위치는 부모가 설정한다.
예를들어 아래의 위젯이 패딩이 있고, 두개의 자식을 레이아웃하고 싶다면...?
[하이, 나는 배치되고 싶은 위젯]
위젯 — 부모님, 제 제약 조건은 무엇입니까?
상위 — 너비는 90~ 300픽셀 이어야 하고 높이는 30~85이여야 합니다.
위젯 — 흠, 5픽셀의 패딩을 원하기 때문에 내 아이들은 최대 290픽셀의 넓이와 75픽셀의 높이를 가질 수 있습니다.
위젯 — Hey first child, 당신은 0~290 넓이, 0~75의 높이를 가져야 합니다.
첫 번째 자식 — 오키. 그러면 너비는 290픽셀로, 높이는 20픽셀로 지정하겠습니다.
위젯 — 흠, 두 번째 아이를 첫 번째 아이 이후에 놓고 싶기 때문에 두 번째 아이를 위한 높이는 55픽셀만 남습니다.
위젯 — hey second child, 당신은 0~290의 넓이를 가지고 0~55의 높이를 가져야 합니다.
두 번째 자식 — 오키, 140픽셀의 넓이 이고 싶고 높이는 30픽셀이 되고 싶어요.
위젯 — 아주 좋습니다. 첫째 아이를 x: 5 and y: 5에, 둘째 아이를 x: 80 and y: 25에 넣겠습니다.
위젯 — 안녕하세요 부모님, 제 크기는 300픽셀의 너비이고 60픽셀의 높이로 결정했습니다.
Container(color: Colors.red)
container의 부모 화면이다. 빨간 container가 화면과 같은 사이즈가 되도록 강제한다. 그래서 화면이 전부 빨갛게 된다.
Container(width: 100, height: 100, color: Colors.red)
빨간 container가 100*100 사이즈로 정의되었다. 하지만 안되쥬? 빨간 container가 화면과 같은 사이즈가 되도록 강제한다.
Center(
child: Container(width: 100, height: 100, color: Colors.red)
)
screen 은 center를 화면과 같은 크기로 강제한다. 그래서 center가 화면을 꽉 채운다.
center는 container가 원하는 사이즈가 되도록 할 수 있다. 단, 화면보다 클 수는 없다. 이제 container는 100*100이 될 수 있다.
Align(
alignment: Alignment.bottomRight,
child: Container(width: 100, height: 100, color: Colors.red),
)
center대신 align을 사용했다는 점에서 앞의 예제와 다르다.
align은 container가 원하는 사이즈가 되도록 할 수 있지만, alignment.bottomRight에 따라 오른쪽 하단에 container를 배치한다.
Center(
child: Container(
color: Colors.red,
width: double.infinity,
height: double.infinity,
)
)
screen 은 center를 화면과 같은 크기로 강제한다. 그래서 center가 화면을 꽉 채운다.
center는 container가 원하는 사이즈가 되도록 할 수 있다. 단, 화면보다 클 수는 없다. container는 infinit한 사이즈를 가지고 싶지만, 화면보다 커질 수 없다.
그래서 화면을 꽉 채울 것이다.
Center(child: Container(color: Colors.red))
screen 은 center를 화면과 같은 크기로 강제한다. 그래서 center가 화면을 꽉 채운다.
center는 container가 원하는 사이즈가 되도록 할 수 있다. 단, 화면보다 클 수는 없다. container는 사이즈가 결정된 child가 없기 때문에 가질 수 있는 가장 큰 사이즈를 가지게 된다.
그래서 화면을 꽉 채울 것입니다.
왜 container는 화면을 꽉 채울까? 간단히 말해 container 위젯을 생성한 사람의 디자인적 결정이었기 떄문이다. (다른 방식으로 생성되었을 수도 있다.) 그러므로 container 도큐먼트를 읽고 container가 동작하는 방식을 이해해야 한다.
Center(
child: Container(
color: Colors.red,
child: Container(color: Colors.green, width: 30, height: 30),
)
)
screen 은 center를 화면과 같은 크기로 강제한다. 그래서 center가 화면을 꽉 채운다.
center는 container가 원하는 사이즈가 되도록 할 수 있다. 단, 화면보다 클 수는 없다. container는 사이즈가 지정되어 있지 않지만 child가 있기 때문에, child의 크기와 같은 사이즈가 된다.
child container와 부모 container는 모두 30*30이 되었다.
child container가 부모 container보다 위에 있기 때문에, 빨간 색을 표시 되지 않는다.
Center(
child: Container(
color: Colors.red,
padding: const EdgeInsets.all(20.0),
child: Container(color: Colors.green, width: 30, height: 30),
)
)
빨간 색 container는 자식 크기에 맞게 크기가 조정되자만 자체 패딩을 고려한다. 따라서 70*70이 되었다.
ConstrainedBox(
constraints: BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: Colors.red, width: 10, height: 10),
)
Containerrk 70~150픽셀 사이가 되어야 한다고 생각 하겠지만 틀렸다! ConstrainedBox는 부모로부터 받은 것보다 추가 제약을 부과한다 .
screen 은 ConstrainedBox가 화면과 같은 크기로 강제한다.
child Container도 화면과 같은 크기라 가정하므로 매개변수를 무시한다.
Center(
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: Colors.red, width: 10, height: 10),
)
)
screen 은 center를 화면과 같은 크기로 강제한다. 그래서 center가 화면을 꽉 채운다.
center는 container가 원하는 사이즈가 되도록 할 수 있다. 단, 화면보다 클 수는 없다. constrainedBox는 추가 제약조건을 부과한다.
따라서 container는 70~150 픽셀 사이여야 한다.
Center(
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: Colors.red, width: 1000, height: 1000),
)
)
center는 constrainedBox이 화면 사이즈내의 모든 사이즈를 허용한다.
constrainedBox는 추가 제약조건을 부과한다.
따라서 1000픽셀 크기가 되고 싶은 container는 150 픽셀이 된다.
Center(
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: Colors.red, width: 100, height: 100),
)
)
center는 constrainedBox이 화면 사이즈내의 모든 사이즈를 허용한다.
constrainedBox는 추가 제약조건을 부과한다.
따라서 1000픽셀 크기가 되고 싶은 container는 100 픽셀이 된다.
(원하는 크기를 가짐)
UnconstrainedBox(
child: Container(color: Colors.red, width: 20, height: 50),
)
screen은 unconstrainedBox가 화면과 같은 크기로 강제한다. 그러나 unconstrainedBox는 container 가 어떤 사이즈든 허용한다.
UnconstrainedBox(
child: Container(color: Colors.red, width: 4000, height: 50),
);
screen은 unconstrainedBox가 화면과 같은 크기로 강제한다. 그리고 unconstrainedBox는 container 가 어떤 사이즈든 허용한다.
안타깝게도 container는 4000픽셀이 되고 싶다. unconstrainedBox안에 맞기엔 너무 크다. 그래서 unconstrainedBox는 'overflow warning'을 표시하게 된다.
OverflowBox(
minWidth: 0.0,
minHeight: 0.0,
maxWidth: double.infinity,
maxHeight: double.infinity,
child: Container(color: Colors.red, width: 4000, height: 50),
);
screen은 OverflowBox가 화면과 같은 크기로 강제한다. 그러나 OverflowBox는 container 가 어떤 사이즈든 허용한다.
OverflowBox는 unconstrainedBox와 비슷하지만 공간이 맞지 않아도 경고를 표시하지 않는 다는 점에서 차이점이 있다.
container는 4000픽셀이 되고 싶고 overflowBox안에 맞기에 너무 크지만, 가능한 것으로 표시된다.(경고 표시되지 않습니다.)
UnconstrainedBox(
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
)
)
이 경우에 아무것도 렌더링하지 않으면서 console에 에러가 표시된다.
unconstrainedBox는 container 가 어떤 사이즈든 허용하지만 container는 무한한 사이즈를 원한다.
flutter는 무한한 사이즈를 렌더링 할 수 없으므로, 에러가 표시된다.
error message : BoxConstraints forces an infinite width.
UnconstrainedBox(
child: LimitedBox(
maxWidth: 100,
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
)
)
)
이렇게 하면 더이상 에러가 발생하지 않는다. 왜냐하면 LimitedBox는 무한한 사이즈를 제공받지만, child에게는 maxWidth 100을 전달하게 된다.
Note. 만약에 unconstrainedBox를 center로 변경한다면 LimitedBox는 limit을 더이상 적용하지 못한다.무한한 제약조건에 대해서만 적용이 가능하지 때문이다. 고로 container는 100이상의 width를 가질 수 있게 된다.
이것이 LimitedBox와 constrainedBox의 차이점을 만든다.
FittedBox(
child: Text('Some Example Text.'),
)
screen 은 FittedBox를 화면과 같은 크기로 강제한다. text는 텍스트의 양, 글꼴, 사이즈 등에 의해 자연스럽게 크기가 결정된다.
fittedBox는 text에게 원하는 사이즈를 가지도록 허용하지만, text의 크기가 지정된 후에는 fittedBox는 사용가능한 모든 너비를 채울 때까지 크기를 조정한다.
Center(
child: FittedBox(
child: Text('Some Example Text.'),
)
)
fittedBox가 center안에 있으면 어떻게 될까? center는 fittedBox가 원하는 크기를 갖도록 허용한다.
fittedBox는 사이즈가 지정되어 있지 않지만 child가 있기 때문에, child의 크기와 같은 사이즈가 된다.
(크기 조정이 일어나지 않음!)
Center(
child: FittedBox(
child: Text('This is some very very very large text that is too big to fit a regular screen in a single line.'),
)
)
만약에 fittedBox가 center에 있고, text가 너무 큰 사이즈이면 어떻게 될까?
fittedBox는 text사이즈가 되도록 시도하지만 화면보다 커질 수 없다. text를 화면에 맞는 사이즈로 리사이즈 하게된다.
Center(
child: Text('This is some very very very large text that is too big to fit a regular screen in a single line.'),
)
위에서 fittedBox를 제거하면, 화면의 최대 너비에 맞도록 줄을 끊는다.
FittedBox(
child: Container(
height: 20.0,
width: double.infinity,
)
)
Note. fittedBox는 경계가 있는 위젯만 크기조정이 가능합니다.(non infinite width and height)
이 경우에 아무것도 렌더링하지 않으면서 console에 에러가 표시된다.
Row(
children:[
Container(color: Colors.red, child: Text('Hello!')),
Container(color: Colors.green, child: Text('Goodbye!)),
]
)
screen 은 row가 화면과 같은 크기로 강제한다.
unconstrainedBox와 마찬가지로 row는 제약조건을 child에게 전달하지 않는다. 그대신 원하는 사이즈를 허용한다. row는 나란히 배치하고 남은 공간을 비어 있게 된다.
Row(
children:[
Container(color: Colors.red, child: Text('This is a very long text that won’t fit the line.')),
Container(color: Colors.green, child: Text('Goodbye!')),
]
)
자식에게 제약을 가하지 않기 때문에 row의 child가 너비에 맞지 않을 정도로 클 수도 있다.
그래서 row는 'overflow warning'을 표시하게 된다.
Row(
children:[
Expanded(
child: Container(color: Colors.red, child: Text('This is a very long text that won’t fit the line.'))
),
Container(color: Colors.green, child: Text('Goodbye!')),
]
)
row의 child가 expand로 감싸지게 되면 row는 child의 width를 정의하지 못하게 된다.
그대신 다른 자식에 의해 정해진 expand의 width를 정의하게 될 것이다.
즉, expand를 사용하면 원래 child의 width는 무의미해지고 무시된다.
Row(
children:[
Expanded(
child: Container(color: Colors.red, child: Text(‘This is a very long text that won’t fit the line.’)),
),
Expanded(
child: Container(color: Colors.green, child: Text(‘Goodbye!’),
),
]
)
만약에 모든 row의 child들이 expand로 감싸지게 되면, 각각의 expand는 균형잡힌 사이즈를 가지게 된다.
즉, expand를 사용하면 원래 child의 선호 사이즈를 무시한다.
Row(children:[
Flexible(
child: Container(color: Colors.red, child: Text('This is a very long text that won’t fit the line.'))),
Flexible(
child: Container(color: Colors.green, child: Text(‘Goodbye!’))),
]
)
Flexible과 expand의 차이점은 단 한가지이다. Flexible는 자식이 자기보다 작거나 같은 width를 가지게 하는 반면 expand는 정확하게 자기와 같은 사이즈를 강제한다.
Flexible와 expand 모두 크기를 조정할 때 자식의 크기를 무시한다.
Note. 크기에 비례하여 row의 child를 확장하는 것이 불가능하다. row은 확장 또는 유연성을 사용할 때 정확한 child을 사용하거나 완전히 무시한다.
Scaffold(
body: Container(
color: blue,
child: Column(
children: [
Text('Hello!'),
Text('Goodbye!'),
]
)))
screen은 Scaffold가 화면과 같은 크기로 강제한다. Scaffold는 화면을 꽉 채우게 된다.
Scaffold는 container가 화면 크기내에서 원하는 사이즈를 허용한다.
Note. 위젯이 자신의 자식에게 어떤 사이즈보다 작아야 한다고 말한다면, 이것은 '느슨한 제약'이라고 말한다. 아래에서 후술한다.
Scaffold(
body: SizedBox.expand(
child: Container(
color: blue,
child: Column(
children: [
Text('Hello!'),
Text('Goodbye!'),
],
))))
만약에 Scaffold의 자식에게 Scaffold와 같은 사이즈여야 한다고 말한다면 sizedBox.expand로 감쌀 수 있다.
Note. 위젯이 자신의 자식에게 어떤 사이즈와 같아야 한다고 말한다면, 이것은 '엄격한 제약'이라고 말한다. 아래에서 후술한다.
제약조건이 엄격하다, 느슨하다는 것은 매우 일반적이므로 알아둘 가치가 있다.
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
위의 두번째 예제(Container #2)를 확인해보면 빨간 container가 화면와 동일한 사이즈를 가지게 강제한다고 말했었다. 이를 container에게 엄격한 제약조건을 적용한 것이다.
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
세번째 예제(Center)를 확인해보면 center는 container가 원하는 사이즈가 되도록 할 수 있다. 단, 화면보다 클 수는 없고 하였다.
center는 container에게 느슨한 제약조건을 전달한다.
궁극적으로 center는 screen(부모)에게 받은 엄격한 제약조건을 container(자식)에게 느슨한 제약조건으로 변환하는 역할을 한다.