Search
Duplicate

CH.4 플러터UI: 주요 위젯, 테마, 레이아웃

목차

CHAPTER POINT

첫 번째 플러터 앱 시작하기
레이아웃 위젯
테마와 스타일링
커스텀 폼 요소
빌더 패턴

Previous

이번 챕터에서 플러트의 모든내장위젯을 다루지는 않습니다. 다만 필요한 위젯을 찾는 방법에 대해 학습한다. 더하여 4장에서는 아래와 같은 사항을 알아보고 학습한다.
앱의 틀을 구성하는 구조 위젯(structural widget)
앱을 꾸미는 테마와 스타일링 기능. 커스텀 색체 조합(color scheme)을 설정하고 스타일링에 사용하는 MediaQuery클래스
레이아웃을 돕는 위젯. Table, Stack, BoxConstraint, TabBar 등 기능을 제공하는 빌딩 블록 위젯
또 다른 레이아웃 위젯인 ListView. 이 위젯은 스크롤을 지원하며 플러터에서 빌더 패턴(builder pattern)을 사용함.

1. 앱 구현과 설정

예제코드는 플러터인액션 서적에서 제공하는 소스를 사용해야하지만, 해당 소스가 버전이 옛날버전이라 그런지 그냥 다운받아서 실행하면 AndroidX 에러가 발생한다. 그래서 따로 해당 프로젝트를 최신화하고 비동기에러를 수정한 버전을 업로드한다. 해당 코드를 참고해 학습하도록 한다.
C:\workspace\flutterSpace\weather_app ├── android ├── assets | └── fonts ├── build ├── ios ├── lib | ├── controllers | | └──forecast_controller.dart //앱에 필요한 저장소의 데이터를 초기화한다. | ├── main.dart //앱의 진입점이다. | ├── models | | └── // 모델 | ├── page | | └── // 페이지 | ├── styles.dart // 이 앱에서 사용할 다양한 색을 쉽게 참조하도록 만든 클래스 | ├── utils | | └── // 다양한 유틸 파일 | └── widget | | └── // 앱의 모든 커스텀 위젯 ├── pubspec.lock ├── pubspec.yaml // 플러터 전용 설정을 포함하는 파일 └── README.md
Plain Text
복사

1.1 pubspec.yaml과 main.dart설정

모든 다트 앱은 설정파일 pubspec.yaml을 포함하는데 앱을 만드는 다트 빌드 시스템은 가장 먼저 pubspec.yaml파일을 찾는다. 플러터 앱을 실행하기 위해선 pubspec.yaml 파일에서 몇 가지 항목을 설정해야 한다.
name: weather_app description: A new Flutter project. publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: sdk: ">=2.7.0 <3.0.0" dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.0 dev_dependencies: flutter_test: sdk: flutter flutter: uses-material-design: true //앱에서 머티리얼 디자인을 사용하기에 플래그는 true fonts: - family: Cabin // 폰트를 임포트할 때 앱에서 참조할 수 있도록 family 지정 fonts: // 3장에서 이미지를 나열했던 것처럼 사용하려는 폰트 나열. - asset: assets/fonts/Cabin-Regular.otf - asset: assets/fonts/Cabin-Bold.otf - asset: assets/fonts/Cabin-Italic.otf - asset: assets/fonts/Cabin-Medium.otf - asset: assets/fonts/Cabin-BoldItalic.otf - asset: assets/fonts/Cabin-MediumItalic.otf - asset: assets/fonts/Cabin-SemiBold.otf - asset: assets/fonts/Cabin-SemiBoldItalic.otf
YAML
복사
wether_app의 pubspec.yaml
다음으로는 프로젝트의 진입점인 main 함수를 포함하는 파일, 즉 진입점을 포함하는 소스코드인 main.dart를 살펴본다.
void main() { WidgetsFlutterBinding.ensureInitialized(); // --- (1) AppSettings settings = AppSettings(); // ---(2) //가로 모드 허용하지 않음. SystemChrome.setPreferredOrientations( // --- (3) [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]) .then((_) => runApp(MyApp(settings: settings))); // --- (4) }
Dart
복사
1.
main에서 비동기함수를 실행할때는 해당 코드를 작성해줘야 한다.
2.
사용자 설정 저장 기능을 흉내 낸 AppSetting 클래스 인스턴스를 만든다.
3.
다음 절에서 SystemChrome 클래스를 살펴본다.
4.
runApp은 반드시 호출해야 하며 이를 루트 위젯으로 전달한다.
현재 앱의 main 함수의 주요 임무는 runApp을 호출하는 것으로 이 앱에는 필요한 몇 가지 설정을 포함한다. 즉 main()함수에서는 어떤 로직이던 결국 runApp 함수를 호출해줘야 한다.
//runApp 함수 기본 코드 main() => runApp(MyApp());
Dart
복사

1.2 SystemChrome

SystemChrome은 네이티브 플랫폼에서 앱이 표시되는 방법을 제어하는 플러터 클래스이며 디바이스를 제어하는데 사용하는 유일한 클래스다. (플러그인을 개발하지 않는다는 조건.)
이 앱에서는 SystemChrome.setPreferredOrientations를 이용해 세로 모드로 앱을 설정했다. 이 클래스는 휴대폰의 오버레이 모양을 제어하는 메서드도 제공한다.
SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, DeviceOrientation.portraitDown ]).then((_) => runApp(MyApp(settings: settings)));
Dart
복사
최초 설정으로 더 이상 건드릴 일이 없기에 다시 살펴볼 필요가 없긴하지만, 더 상세한 내용이 궁금하다면 여기를 참고하자.
다트 퓨처
다트의 비동기 프로그래밍은 챕터 9에서 다시 설명하겠지만 퓨처(Future)는 다트의 모든 비동기 프로그래밍의 기초를 구성하는 클래스다.
자바스크립트의 프로미스와 마찮가지로 퓨처에서는 then을 사용할 수 있다.
myFutureMethod().then((returnValue) => ... do some code ... );
Dart
복사
사용 예
Future<Burger> orderBurgerFromServer() async { return await prepareBurger(); } orderBurgerFromServer() .then((Burger burger) => eatBurger(burger));
Dart
복사
orderBurgerFromServer 메서드는 Burger 서브형식을 갖는 Furture 형식을 반환한다.
그리고 해당 메서드가 완료한 결과를 then 콜백의 인수로 전달한다.

2. 구조 위젯과 기타 설정

플러터로 프로젝트를 구성하면 자주사용하는 정도가아니라 거의 무조건 사용하는 위젯이 몇가지 있다. 지금부터 항상 사용하는 MaterialApp, Scaffold, AppBar, Theme을 학습한다.

2.1 MaterialApp 위젯

네비게이터 설정, 앱 전반의 테마 사용등 대부분 모바일 앱에 필요한 기능을 추상화해 제공하는 위젯 WidgetsApp이 있습니다. 하지만 이를 사용하기 위해서는 더 많은 부분을 직접 구현하고 작업해야 한다. 그래서 이를 상속하는 위젯이 머티리얼 디자인을 제공하는 MaterialApp과 IOS 디자인을 제공하는 CupertinoApp이 있다.
MaterialApp은 WidgetsApp에 비해 편리하게 사용이 가능하다.
머티리얼 디자인 전용 기능과 스타일 옵션을 제공한다.
페이지 이동에 필요한 애니메이션이나 기타 머티리얼 위젯 컬렉션에 포함된 위젯을 자유롭게 사용할 수 있다.
머티리얼 스타일 가이드라인을 따른다. (1)
머티리얼 디자인 가이드라인을 사용하지 않는다 해도 MaterialApp 은 여전히 유용하다.
void main() { // 앱 진입점 WidgetsFlutterBinding.ensureInitialized(); AppSettings settings = AppSettings(); // Don't allow landscape mode SystemChrome.setPreferredOrientations( [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]) .then((_) => runApp(MyApp(settings: settings))); } class MyApp extends StatelessWidget { final AppSettings settings; const MyApp({Key key, this.settings}) : super(key: key); Widget build(BuildContext context) { //... return MaterialApp( // --- (1) title: 'Weather App', debugShowCheckedModeBanner: false, // --- (2) theme: theme, // --- (3) home: PageContainer(settings: settings), // --- (4) ); } }
Dart
복사
weather_app/lib/main.dart
1.
MaterialApp을 반환한다.
2.
앱을 개발하는동안 화면에 나타나는 배너를 제거하는 플래그.
3.
MaterialApp은 앱 전체의 테마를 처리한다.
4.
home은 앱의 홈페이지를 가리키며 모든 위젯은 home이 될 수 있다. PageContainer는 날씨 앱을 구현하는 도중 만든 커스텀 위젯이다.
1. 머티리얼 디자인 스펙(https://material.io/design)

2.2 Scaffold 위젯

MaterialApp 위젯이 앱의 설정과 기능을 제공하는 위젯이라면 Scaffold는 앱 구조를 만드는 일을 돕는다. MaterialApp이 앱의 배관, 전기라면 Scaffold는 뼈대와 벽이라 할 수 있다.
드로어(drawer) : 한쪽 모서리에서 미끄러지듯 등장하는 요소로 메뉴를 구현할 때 자주 사용
하단 시트(bottom sheet): 화면의 아래쪽에서 미끄러지듯 등장하는 요소로 ios 앱에서 자주 사용.
위와 같은 요소들을 추가하는 기능을 제공한다.
여기서 따로 설정하지 않으면 Scaffold의 AppBar는 앱 왼쪽 윗부분에 메뉴 버튼을 기본으로 표시하며 이 버튼을 누르면 드로어가 열린다. 메뉴를 포함하지 않는 화면에서는 백 버튼이 나타난다.
물론, 해당 기능이 필요 없어서 앱에 드로어 스타일 메뉴를 사용하지 않기 위해 드로어를 전달하지 않으면 메뉴 버튼도 사라진다.
Scaffold 위젯은 생성자로 설정할 수 있는 다양한 선택형 기능을 제공한다.
const Scaffold({ Key key, this.appBar, this.body, this.floatingActionButton, this.floatingActionButtonLocation, this.floatingActionButtonAnimator, this.persistentFooterButtons, this.drawer, this.endDrawer, this.bottomNavigationBar, this.bottomSheet, this.backgroundColor, this.resizeToAvoidBottomPadding = true, this.primary = true, }) : assert(primary != null), super(key: key);
Dart
복사
Scaffold의 모든 프로퍼티 목록
모든 프로퍼티가 @required가 없어서 필수적이 아니며 선택적이다. 그렇기에 필요한 프로퍼티만 사용하면 된다.
날씨앱(weather_app)에서 화면을 담당하는 dart소스(forecas_page, settings_page, add_city_page ..)들을 살펴보면 모두 각각의 Scaffold위젯을 갖는다. 이 중 forecast_page의 Scaffold를 확인해보면 10개가 넘는 생성자의 지정 인수중에서 appBar와 body만 사용하고 있다. 이처럼 위젯은 다양한 기능을 제공하지만 자유롭게 원하는 기능만 사용할 수 있다.

2.3 AppBar 위젯

AppBar의 기능을 제공하는 위젯
보통 Scaffold의 appbar 프로퍼티에 사용하며 화면 위쪽에 특정 높이의 공간을 차지한다.
Scaffold의 프로퍼티로 사용되고 drawer인수가 not null이라면 자동으로 메뉴 버튼을 추가한다.
'뒤로' 갈 수 있는 화면이라 판단되면 앱의 Navigator는 자동으로 백 버튼을 추가한다.
AppBar는 다양한 위젯을 인수로 받을 수 있는데 각각의 인수는 특정 위치에 대응한다.
메뉴 버튼과 백 버튼을 처리하는 프로퍼티를 리딩 액션(leading action)이라 부른다.
leading, automaticallyImplyLeading 프로퍼티를 설정할 수 있다.

PreferredSize 위젯

AppBar 클래스는 명시적으로 너비와 높이를 설정할 수 있는 PreferredSize 위젯을 상속받는다.
Scaffold.appBar 프로퍼티는 제약 조건을 설정하기 전 AppBar의 크기를 알 수 있도록 PreferredSize 클래스 위젯을 요구한다.
(날씨 앱에서는 Scaffold.appBar 인수를 사용하지 않고 직접 PreferredSize를 사용한다.)
return Scaffold( appBar: PreferredSize( //---(1) preferredSize: Size.fromHeight(ui.appBarHeight(context)),// --- (2) child: TransitionAppbar(...) // ---(3) ), );
Dart
복사
1.
Scaffold.appBar 프로퍼티에 PreferredSize를 사용하고 다른 위젯을 추가한다. AppBar는 PreferredSize를 상속받고 Scaffold.appBar에는 AppBar보다 PreferredSize를 활용한다.
2.
PreferredSize의 첫 인수 preferredSize는 너비와 높이를 정의하는 Size 클래스를 받는다.
3.
PreferredSize의 두 번째 인수는 child이며 이 예제에서는 커스텀 애니메이션을 포함하는 AppBar, 즉 TransitionAppBar위젯을 사용했다.

3. 스타일링과 테마

Theme 위젯으로 색, 폰트 스타일, 버튼 스타일 등 다양한 앱 기본 스타일을 설정할 수 있다.

3.1 Theme 위젯

Theme 위젯으로 자동으로 앱 전체에 스타일을 적용한다.
1.
색 관련 프로퍼티
brightness(어두운 테마나 밝은 테마로 설정)
primarySwatch
primaryColor
accentColor
2.
특정 기능을 제어하는 프로퍼티
canvasColor
scaffoldBackgroundColor
dividerColor
cardColor
buttonColor
errorColor
이외에도 더 많은 프로퍼티(20개++)가 있는데 이를 모두 신경쓰기는 쉽지 않다.
그렇기에 플러터는 MaterialApp을 앱의 루트로 사용할 때 모든 프로퍼티에 기본값을 할당해 개발자가 필요한 프로퍼티만 오버라이드 할 수 있도록 제공한다.

앱에 테마 사용하기

MaterialApp.theme 프로퍼티에 ThemeData객체를 전달해 테마를 추가한다.
혹은 Theme 위젯을 직접 만들어 ThemeData객체에 전달한다.
final theme = ThemeData( fontFamily: "Cabin", primaryColor: AppColor.midnightSky, // --- (1) accentColor: AppColor.midnightCloud, primaryTextTheme: Theme.of(context).textTheme.apply( // --- (2) bodyColor: AppColor.textColorDark, displayColor: AppColor.textColorDark, ), textTheme: Theme.of(context).textTheme.apply( bodyColor: AppColor.textColorDark, displayColor: AppColor.textColorDark, ), );
Dart
복사
weather_app/lib/main.dart의 build부분
1.
다양한 커스텀 색을 사용하므로 AppColor 클래스를 만들어서 사용한다.
2.
Theme클래스의 apply메서드로 현재 테마를 복제한 다음 프로퍼티를 바꾼다.
BuildContext는 위젯 트리에서 위젯의 위치정보를 제공한다. 그렇기에 Theme을 포함해 트리에 상위에 위치한 위젯 정보를 얻을 수 있는데, 이를 이용해 이 위젯이 속한 트리에서 가장 가까운 ThemeData에 할당된 속성이나 색상을 알아서 업데이트를 할 수 있다.

3.2 MediaQuery와 of 메서드

플러터에서는 논리적 픽셀(logical pixel) 한 가지 단위만 사용하여 대부분의 레이아웃 크기 문제를 해결해야 한다. 플러터에서는 퍼센트를 사용할 수 없기에 MediaQuery 위젯을 이용해 화면 크기를 먼저 알아내야 한다.
BuildContext를 이용해 앱 어디서든 MediaQuery 위젯을 사용할 수 있다.
// BuildContext를 이용해 해당 위젯의 너비를 구하기 final width = MediaQuery.of(context).size.width;
Dart
복사
of 메서드는 트리에서 가장 가까운 MediaQuery 클래스의 레퍼런스를 반환한다.
(이때 of 메서드는 정적 메서드이기에 MediaQuery 클래스의 인스턴스를 만들지 않고 직접 호출한다.)
이처럼 위젯의 buildContext는 트리에서 위젯의 위치 정보를 플러터에 제공한다. 모든 of 메서드가 cotext를 인수로 받는 이유가 이 때문이다.
앱을 실행하는 물리적 디바이스의 정보를 얻거나 디바이스를 제어할 때 MediaQuery를 사용한다.
현재 휴대폰이 세로 방향인지 가로 방향인지 확인할 경우
접근성과 관련해 애니메이션을 비활성화하고 색을 반전시킬 경우
사용자가 텍스트 크기를 확대했는지 확인할 경우
전체 앱에 패딩을 설정할 경우
날씨 앱에서는 MediaQuery를 이용해 화면 크기에 따라 적절한 크기로 위젯을 설정한다.

3.3 screenAwareSize 메서드

return Scaffold( appBar: PreferredSize( preferredSize: Size.fromHeight(ui.appBarHeight(context)), child:... ), ),
Dart
복사
forecast_page의 build메서드
Size.fromHeight는 정해진 높이와 무한대의 너비를 갖는 Size객체를 만드는 생성자로 ui.appBarHeight 메서드의 반환값을 사용한다. 날씨 앱의 utils 패키지에 있는 flutter_ui_uitils.dart 파일에서 ui.appBarHeight(context)의 구현코드를 확인할 수 있다.
final double kToolbarHeight = 56.0; double appBarHeight(BuildContext context) { // --- (1) return screenAwareSize(kToolbarHeight, context); } const double kBaseHeight = 650.0; double screenAwareSize(double size, BuildContext context) { double drawingHeight = MediaQuery.of(context).size.height - MediaQuery.of(context).padding.top;// ---(2) return size * drawingHeight / kBaseHeight; }
Dart
복사
flutter_ui_utils.dart
1.
Context로 MediaQuery 정보를 얻을 수 있도록 Context를 전달해 받는다.
2.
context에서 현재 위젯에 가장 가까운 MediaQuery를 가져와 크기정보를 얻는다.
다시 forecast_page의 build메서드 코드중 Scaffold부분으로 돌아가서 Size.fromHeight(ui.appBarHeight(context)) 이 코드는 Scaffold에 앱 바의 크기를 알려준다. 특히 모든 화면의 크기에 알맞은 높이를 갖는 Size 인스턴스를 전달한다.

4. 자주 사용하는 레이아웃과 UI 위젯

4.1 Stack 위젯

한 위젯 위에 다른 위젯을 쌓아올릴 때 사용하는 위젯.
스택 API를 이용해 화면의 스택 경계에서 정확히 어떤 위치에 위젯을 추가할지를 설정한다.
날씨 앱에서는 스택을 이용해 현재 시간, 날씨를 배경 이미지에 반영한다.
태양, 구름, 달등의 콘텐츠는 모두 다른 위젯으로 스택에 추가된다.
위치를 지정하지 않은 자식을 Column이나 Row가 자식을 취급하는 것처럼 처리한다.
자식 위젯을 왼쪽 위 모서리로 정렬하여 이들을 나란히 놓는다.
alignment 프로퍼티로 정렬방향을 설정한다.
(ex: horizontal을 이용하면 Stack이 자식을 Row처럼 정렬한다.)
Positioned 위젯으로 위젯을 감싸위치를 지정한다.
위치를 지정한 위젯은 top, left, right, bottom, widgth, height등의 프로퍼티를 갖는다.
이 프로퍼티로 위젯을 그릴 위치를 설정하면 RenderStack 알고리즘이 자식을 그린다.
1.
우선 위치를 지정하지 않은 자식을 Row나 Column처럼 처리한다. 이 때 스택의 최종 크기가 결정된다. 위치를 지정한 자식이 없으면 스택은 가능한 큰 공간을 차지한다.
2.
스택의 렌더 상자에 비례해 위치를 지정한 모든 자식을 배치한다. 위치를 지정한 위젯의 속성으로 스택의 자식들을 평행한 경계에서 어디에 위치시킬지 설정한다.
3.
모든 위젯의 배치를 마쳤으면 스택의 '바닥'에 위치한 위젯부터 차례대로 그린다.
이번에는 완성된 날씨앱의 forecast_page의 Stack 코드를 분석해보자.
Stack( children: <Widget>[ // ---(1) SlideTransition(// --- (2) position: _positionOffsetTween.animate( // --- (3) _animationController.drive( CurveTween(curve: Curves.bounceOut), ), ), child: Sun( animation: _colorTween.animate(_animationController), ), ), SlideTransition( position: _cloudPositionOffsetTween.animate( _weatherConditionAnimationController.drive( CurveTween(curve: Curves.bounceOut), ), ), child: Clouds( // --- (4) isRaining: isRaining, animation: _cloudColorTween.animate(_animationController), ), ), Column( // --- (5) verticalDirection: VerticalDirection.up, children: <Widget>[ forecastContent, mainContent, Flexible(child: timePickerRow), ], ), ], ),
Dart
복사
1.
Stack은 Row나 Column처럼 여러 자식(children)을 갖는다.
2.
배경에 태양과 달을 그리는 위젯 position 프로퍼티를 포함한다.
3.
position 프로퍼티는 위젯의 위치를 명시적으로 지정한다는 점에서 Positioned.position과 비슷하지만 SlideTransition의 Poisitioned 프로퍼티는 바꿀 수 있는 값을 갖는다는 점이 다르다.
4.
Stack의 두 번째 자식은 Clouds이며 이 위젯은 태양 위에 그려진다.
5.
Stack의 최상위 레이어의 콘텐츠다.

4.2 Table 위젯

weather_app의 Table 위젯
Table은 위젯을 행과 열로 배치한다.
표의 각 셀은 같은 행의 다른 셀과 같은 높이를 가지며 같은 열의 다른 셀과 같은 너비를 갖는다.
빈 Table 셀은 존재할 수 없기에 열의 너비를 명시적으로 설정해야 한다.
Table( columnWidths: Map<int, TableColumnWidth>{}, // --- (1) border: Border(), // --- (2) defaultColumnWidth: TableColumnWidth(), // --- (3) defaultVerticalAlignment: TableCellVerticalAlignment(), // --- (4) children: List<TableRow>[]// --- (5) );
Dart
복사
1.
0 번째 행에서 시작하는 각 열의 너비 Map이다.
2.
전체 Table의 경계다.
3.
열의 너비를 명시적으로 설정하지 않은 열의 기본 너비이다.
4.
셀의 콘텐츠를 어떻게 정렬할지 설정하는 선택형 인수이다.
5.
Table 행 리스트다. 테이블은 행 리스트를 자식으로 가지며 각 행은 여러 자식들(셀)로 구성된다.

Table 사용시 주의사항

columnWidths는 전달할 필요가 없지만 defaultColumnWidth는 null로 설정할 수 없다.
defaultColumnWidth는 기본 인숫값으로 FlexColumnWidth(1.0)을 가지므로 아무 값도 전달하지 않아도 괜찮지만 null로 설정할 수 없으며 null로 설정하면 오류가 발생한다.
columnWidths로 열의 너비를 설정한다. 맵은 열의 인덱스(0부터 시작)와 열의 너비를 키로 갖는다.
children인수는 List<TableRow>를 기대하므로 아무 위젯이나 전달할 수 없다.
Border는 선택형이다.
행의 자식이 TableCell이어야 TableCellVerticalAlignment가 동작한다.
columnWidth는 모두 주어진 공간내에서 최대한을 확보하려 한다. 이 부분을 응용하면, 특정 행을 유연하게 만들어줄 수도 있다.
4개의 열중 3개의 열의 크기가 명시적으로 정해진다면 남은 하나의 열은 남은 공간을 최대한 차지한다.
child: Table( border: TableBorder.all(width: 2.0, color: const Color(0xFFFFFFFF)), columnWidths: { 0: FixedColumnWidth(100.0), 2: FixedColumnWidth(20.0), 3: FixedColumnWidth(20.0), }, defaultVerticalAlignment: TableCellVerticalAlignment.middle, children:<TableRow>[...], );
Dart
복사
날씨앱에서 날짜별 정보는 4개의 열(요일, 아이콘, 최고온도, 최저온도)을 가진다. 그런데 코드를 보면 0, 2, 3번째 즉, 첫 번째와 세 번째, 네번째 열의 크기만 각각 지정해주고 있다.
그럼 남은 열은 명시적으로 지정하고 남은 크기를 최대한 가져감으로써 유연하게 크기를 갖는다.
더하여, TableCellVerticalAlignment.middle 속성은 셀의 콘텐츠를 셀을 세로로 볼 때 중간에 위치하도록 하는 속성이다.
마지막으로 TableRow를 살펴본다. TableRow는 일반 행보다 간단한데 두 가지 특징이 있다.
Table의 모든 행은 같은 수의 자식을 가져야 한다.
자식의 서브 위젯 트리에 TableCell을 반드시 사용할 필요는 없다. TableCell은 TableRow의 직계 자식이어야 할 필요는 없으며 위젯 트리의 어딘가에 TableRow를 조상으로 가지면 충분하다.

Dart의 List.generate() 생성자로 위젯 생성

Table에서는 children 프로퍼티에 각각의 TableRow정보를 전달하는데, 이를 직접 하나하나 전달하는 대신 위젯을 반환하는 함수, 생성자, 클래스를 사용할 수 있다.
Table( border: TableBorder.all(width: 2.0, color: const Color(0xFFFFFFFF)), columnWidths: { 0: FixedColumnWidth(100.0), 2: FixedColumnWidth(20.0), 3: FixedColumnWidth(20.0), }, defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: List.generate(7, (int index) { // --- (1) ForecastDay day = forecast.days[index]; // --- (2) Weather dailyWeather = forecast.days[index].hourlyWeather[0]; // --- (3) var weatherIcon = _getWeatherIcon(dailyWeather);// --- (4) return TableRow( // --- (5) children: [ TableCell(...), ... ), ], ); }), ),
Dart
복사
1.
다트 List 클래스 생성자로 리스트에 추가할 항목의 숫자(int)와 이들 항목을 생성하는 콜백을 인수로 받는다. 그리고 콜백은 현재 index를 인수로 받으며 인수로 전달한 int 횟수만큼 반복한다.
(예제 코드에서는 7회 반복한다.)
2.
표 셀에 표시할 데이터, 인덱스로 표의 각 행에 다른 데이터를 표시한다.
3.
필요한 추가 데이터로 현재 온도를 표시할 때 사용하는 시간별 날씨를 제공한다.
4.
현재 날씨에 따라 알맞은 아이콘을 반환한다.
5.
생성된 리스트에 현재 인덱스에 삽입할 수 있는 위젯을 반환한다.
이제, 마지막으로 Table 위젯을 트리로 추가하는 코드를 살펴보도록 한다.
Column( // --- (1) verticalDirection: VerticalDirection.up, // --- (2) children: <Widget>[ forecastContent, // --- (3) mainContent, // --- (4) Flexible(child: timePickerRow), ], ),
Dart
복사
forecast_page.dart의 build부분
1.
forecast 페이지의 모든 컨텐츠를 포함한다.
2.
첫 번째 자식 위젯이 바닥에 위치하도록 열의 순서를 역전시킨다.
3.
Table 위젯을 가리키는 변수이다.
4.
날씨 앱의 추가 위젯이다.

4.3 TabBar 위젯

내장 TabBar 위젯의 자식들은 스크롤할 수도 있고, 수평의 뷰로 구성되며 선택할 수 있는(tappable)기능을 갖는다.
탭 바의 위젯을 탭하면 탭 바 위젯에 전달한 콜백이 호출된다.
이 콜백을 이용해 탭 바의 자식 위젯은 페이지의 위젯을 바꾼다.
플러터의 탭 관련 위젯 다이어그램
대화형 탭 바 개요
위 사진에서는 사용자가 선택할 수 있도록 시간을 표시하는 자식 위젯과 필요한 기능을 처리하는 TabController, 이 두가지 핵심 기능을 포함한다.

TabController 위젯

플러터에서 많은 대화형 위젯은 이벤트를 관리할 수 있도록 관련 컨트롤러를 갖는다.
(ex:사용자의 I/O 입력을 처리하는 TextEditingController가 존재한다.)
TabBar에는 TabController를 상요하여 사용자가 새 탭을 선택했을 때 앱이 필요한 콘텐츠를 갱신하도록 알리는 역할을 담당한다.
날씨 앱에서 lib/widget/time_picker_row.dart 파일을 살펴보며 확인해보자.
class TimePickerRow extends StatefulWidget { final List<String> tabItems; // --- (1) final ForecastController forecastController; // --- (2) final Function onTabChange; // --- (3) final int startIndex; // --- (4) ... }
Dart
복사
1.
전달된 프로퍼티 저장. 예제 위젯은 시간대를 표시하는 문자열 리스트를 받는다.
2.
날씨 데이터를 쉽게 가져올 수 있게 돕는 클래스.
3.
부모가 전달한 콜백, 예제에서는 새 탭을 선택했을 때 이를 알리는데 사용한다.
4.
기본으로 선택된 탭을 TabBar에 알림. 예제에서는 현재 시간을 가리키는 위젯을 설정한다.
class TimePickerRow extends StatefulWidget { final List<String> tabItems; final ForecastController forecastController; final Function onTabChange; final int startIndex; const TimePickerRow({ Key key, this.forecastController, this.tabItems, this.onTabChange, this.startIndex, }) : super(key: key); _TimePickerRowState createState() => _TimePickerRowState(); } class _TimePickerRowState extends State<TimePickerRow> with SingleTickerProviderStateMixin { // --- (1) TabController _tabController; void initState() { _tabController = TabController( // --- (2) length: AnimationUtil.hours.length, vsync: this,// --- (3) initialIndex: widget.startIndex, ); _tabController.addListener(handleTabChange); // --- (4) super.initState(); } void handleTabChange() { if (_tabController.indexIsChanging) return; // --- (5) widget.onTabChange(_tabController.index); } //... }
Dart
복사
1.
애니메이션을 지원하는 프로퍼티를 가질 것임을 가리키는 믹스인(mixin)이다. TabBar는 내장 애니메이션을 포함하므로 이 믹스인이 필요하다.
2.
탭 기능을 처리할 탭 컨트롤러를 선언한다. 생성자에서 만들어진다.
3.
컨트롤러를 만든다. TabController는 몇 개의 탭이 있는지 알아야 한다.
4.
애니메이션 관련 내용
5.
탭이 바뀌면 콜백을 실행하도록 컨트롤러에 리스너를 추가한다.
6.
애니메이션 중간에 새 이벤트가 발생하는 것을 방지한다.

리스너

: 비동기 기능을 실행할 때 활용하는 기법으로 특별한 객체거나 다른 형식을 갖지 않는다.
플러터 라이브러리에서 리스너, 변경 알람(change modifier), 스트림(stream)등의 용어를 자주 접하게 될텐데, 이는 모두 옵저버블(observable)이라는 같은 종류의 프로그래밍 컨셉을 구현한다.
보통 어떤 이벤트가 발생할 때 실행되는 함수를 리스너라 부른다. 이 함수는 누군가 리스너가 실행되야한다고 알려주기를 기다린다. (옵저빙 한다는 의미로 옵저버블에 걸맞다.)
해당 주제에서는 탭을 바꾸면 탭 컨트롤러의 addListener 함수가 호출된다. 이를 통해 사용자가 탭을 바꾸면 값이나 상태를 갱신할 수 있다.
void handleTabChange() { if (_tabController.indexIsChanging) return; // --- (5) widget.onTabChange(_tabController.index); }
Dart
복사
TabController는 리스너뿐 아니라 탭과 관련 콘텐츠를 관리하도록 돕는 게터(getter)를 갖는다.
위 코드와 같이 해당 메서드에서 현재 선택된 탭이 어떤것인지 앱에 알린다.
여기서 중요한 부분은 setState 인데, 날씨 앱에서 다른 시간을 선택하면 그시간에 해당하는 날씨로 UI를 다시 그린다. 이는 setState 가 픞ㄹ러터에 새로 선택된 탭에 맞는 화면을 다시 그리도록 명령하기 때문이다.
TabController.index 게터는 현재 선택된 탭을 가르킨다. TabController는 탭 정보를 가져오고 현재 선택된 탭 정보를 갱신하는 역할만 할 뿐이다.

TabBar 위젯 실습

이제 TabBar 위젯이 구성되는 time_picker_row.dart파일의 build메서드를 살펴보자.
Widget build(BuildContext context) { return TabBar( labelColor: Colors.black,// --- (1) unselectedLabelColor: Colors.black38, unselectedLabelStyle: Theme.of(context).textTheme.caption.copyWith(fontSize: 10.0), labelStyle: Theme.of(context).textTheme.caption.copyWith(fontSize: 12.0), indicatorColor: Colors.transparent, labelPadding: EdgeInsets.symmetric(horizontal: 48.0, vertical: 8.0), controller: _tabController, // --- (2) tabs: widget.tabItems.map((t) => Text(t)).toList(), // --- (3) isScrollable: true, // --- (4) ); }
Dart
복사
1.
스타일을 정의하는 TabBar설정 옵션이다.
2.
부모가 위젯에 TabController를 전달한다.
3.
ForecastPage에서 tabItems를 전달한다.
4.
기본적으로 탭은 스크롤할 수 없다. 이 프로퍼티를 true로 하면 스크롤할 수 있다.

TabBar 요점

탭을 구현하려면 TabController와 자식 위젯이 필요하다.
탭 바의 위젯을 탭하면 콜백을 통해 탭을 전환할 수 있다. 콜백은 TabController가 제공하는 프로퍼티를 사용해 플러터에 새 탭을 그리도록 지시한다.

5. ListView와 빌더

ListView는 자주 사용할 뿐 아니라 효과적인 플러터 앱을 구현하는데 필요한 패턴과 개념을 포함한다.
ListView는 스크롤을 지원한다.
자식의 수를 알 수 없을 때 ListView를 사용한다.
(ex: todolist 앱에서 ListView로 목록을 표시하면 할일 항목이 하나도 없거나 많이 존재할 수 있다.)
결국 ListView는 스크롤할 수 있는 Column이나 Row다.
날씨앱의 설정페이지

빌더 패턴

플러터 전체에서 빌더 패턴은 자주 볼 수 있는데, 필요한 위젯을 만들 때 사용한다.
ListView.builder 생성자는 itemBuilder 프로퍼티로 콜백을 받으며 이 콜백을 통해 위젯을 반환한다. 특히 리스트에 표시해야 할 항목의 수가 아주 많거나 무한대라면 빌더 덕분에 효과적으로 화면에 항목을 그릴 수 있다. (플러터는 화면에 보이는 항목만 그린다.)
실제로 스크롤을 하며 상태가 바뀔때마다 모든 트윗을 다시 그리는것은 불가능하기에 플러터는 사용자가 뷰에서 스크롤할 때마다 필요한 항목만 그린다. ListView에서 이런 기능을 기본적으로 제공한다. 날씨 앱의 settings_page.dart 파일을 보며 ListView 빌더 코드를 살펴보자.
Expanded( // --- (1) child: ListView.builder( shrinkWrap: true, // --- (2) itemCount: allAddedCities.length, // --- (3) itemBuilder: (BuildContext context, int index) { // --- (4) final city = allAddedCities[index]; return Dismissible( onDismissed: (DismissDirection dir) => _handleDismiss(dir, city), confirmDismiss: (DismissDirection dir) async => dir == DismissDirection.endToStart, background: Container( child: Icon(Icons.delete_forever), decoration: BoxDecoration(color: Colors.red[700]), ), key: ValueKey(city), child: CheckboxListTile( // --- (5) value: city.active, title: Text(city.name), onChanged: (bool b) => _handleCityActiveChange(b, city), // --- (6) ), ); }, ), )
Dart
복사
1.
ListView는 메인축으로 가능한 크게 확장한다. Column의 자식이므로 Expanded가 별도의 제한을 갖지 않으면 크기는 무한대로 확장한다.
2.
무한 크기 오류를 방지하는 한 가지 방법으로 ListView에 자식 크기만큼 확장하라고 지시한다.
3.
빌더는 자신이 만들 항목의 개수를 알아야 한다.
4.
BuilderContext와 인덱스를 인수로 받는 콜백으로 플러터에서 자주 사용하는 빌더 함수이다.
5.
플러터에서 제공하는 편리한 위젯으로 체크 박스를 표시한다.
6.
체크 박스를 제어한다. 항목을 체크하면 전달한 함수가 호출된다.
데이터를 알 수 없는 상황에서 데이터를 표시하는 위젯을 플러터로 만들 때 이러한 빌더 패턴을 사용하는데 ListView는 다음과 같은 다양한 생성자를 제공한다.
ListView.separated는 ListView.builder와 비슷하지만 두 가지 빌더 메서드를 받는다. 한 가지는 리스트 항목을 만들 때 사용하고 나머지 한 가지는 리스트 항목 간의 분리자를 만드는 데 사용한다.
ListView.custom을 이용하면 커스텀 자식으로 리스트 뷰를 만든다. 빌더처럼 간단하지 않은데, 리스트 뷰의 항목을 각자 다른 항목으로 만들어야 한다고 할 때 사용하며 자식을 어떻게 그려야 할지 제어할 수 있다.

6. 정리

플러터는 MaterialApp, Scaffold, AppBar등 다양하고 편리한 기능을 가진 구조를 만들 수 있는 위젯을 제공한다.
SystemChrome클래스로 앱을 가로 또는 세로 모드로 설정하는 등의 디바이스 기능을 사용한다.
MediaQuery를 이용해 화면 크기 정보를 얻는다. 특히 화면 크기에 따라 위젯의 크기를 조절할 때 유용하다.
Theme으로 스타일 프로퍼티를 설정하면 앱 전체 위젯에 영향을 준다.
Stack위젯으로 화면의 위젯을 원하는 위치에 쌓는다.
Table 위젯으로 표 형식의 위젯을 배치한다.
ListView와 빌더를 이용하면 간단하게 무한대의 항목을 가진 리스트를 만들 수 있고 동시에 빠른 성능도 보장한다.