소스를 올리고 싶은데... flutter in action에서 제공하는 소스코드가 nullsafe 이전 버전인데다 상속받아 만든 위젯이 너무 많다... 가져다 쓰다가 피보는 중 ㅜ
처음부터 다시 만드는게 덜 고생스러울듯... 일단 스탑!
flutter in action github
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),
],
),
],
),
Table(
columnWidths: Map<int, TableColumnWidth>{}, // --- (1)
border: Border(), // --- (2)
defaultColumnWidth: TableColumnWidth(), // --- (3)
defaultVerticalAlignment:
TableCellVerticalAlignment(), // --- (4)
children: List<TableRow>[]// --- (5)
);
🧨Table 사용시 주의사항🧨
- columnWidths는 전달할 필요가 없지만 defaultColumnWidth는 null로 설정할 수 없다.
- defaultColumnWidth는 기본 인숫값으로 FlexColumnWidth(1.0)을 가지므로 아무 값도 전달하지 않아도 괜찮지만 null로 설정할 수 없으며 null로 설정하면 오류가 발생한다.
- columnWidths로 열의 너비를 설정한다. 맵은 열의 인덱스(0부터 시작)와 열의 너비를 키로 갖는다.
- children인수는 List를 기대하므로 아무 위젯이나 전달할 수 없다.
- 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>[...],
);
날씨앱에서 날짜별 정보는 4개의 열(요일, 아이콘, 최고온도, 최저온도)을 가진다. 그런데 코드를 보면 0, 2, 3번째 즉, 첫 번째와 세 번째, 네번째 열의 크기만 각각 지정해주고 있다.
그럼 남은 열은 명시적으로 지정하고 남은 크기를 최대한 가져감으로써 유연하게 크기를 갖는다.
더하여, TableCellVerticalAlignment.middle 속성은 셀의 콘텐츠를 셀을 세로로 볼 때 중간에 위치하도록 하는 속성이다.
마지막으로 TableRow를 살펴본다. TableRow는 일반 행보다 간단한데 두 가지 특징이 있다.
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(...),
...
),
],
);
}),
),
Column( // --- (1)
verticalDirection: VerticalDirection.up, // --- (2)
children: <Widget>[
forecastContent, // --- (3)
mainContent, // --- (4)
Flexible(child: timePickerRow),
],
),
내장 TabBar 위젯의 자식들은 스크롤할 수도 있고, 수평의 뷰로 구성되며 선택할 수 있는(tappable)기능을 갖는다.
탭 바의 위젯을 탭하면 탭 바 위젯에 전달한 콜백이 호출된다.
위 사진에서는 사용자가 선택할 수 있도록 시간을 표시하는 자식 위젯과 필요한 기능을 처리하는 TabController, 이 두가지 핵심 기능을 포함한다.
플러터에서 많은 대화형 위젯은 이벤트를 관리할 수 있도록 관련 컨트롤러를 갖는다.
(ex:사용자의 I/O 입력을 처리하는 TextEditingController가 존재한다.)
TabBar에는 TabController를 상요하여 사용자가 새 탭을 선택했을 때 앱이 필요한 콘텐츠를 갱신하도록 알리는 역할을 담당한다.
class TimePickerRow extends StatefulWidget {
final List<String> tabItems; // --- (1)
final ForecastController forecastController; // --- (2)
final Function onTabChange; // --- (3)
final int startIndex; // --- (4)
...
}
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);
}
//...
}
: 비동기 기능을 실행할 때 활용하는 기법으로 특별한 객체거나 다른 형식을 갖지 않는다.
플러터 라이브러리에서 리스너, 변경 알람(change modifier), 스트림(stream)등의 용어를 자주 접하게 될텐데, 이는 모두 옵저버블(observable)이라는 같은 종류의 프로그래밍 컨셉을 구현한다.
보통 어떤 이벤트가 발생할 때 실행되는 함수를 리스너라 부른다. 이 함수는 누군가 리스너가 실행되야한다고 알려주기를 기다린다. (옵저빙 한다는 의미로 옵저버블에 걸맞다.)
해당 주제에서는 탭을 바꾸면 탭 컨트롤러의 addListener 함수가 호출된다. 이를 통해 사용자가 탭을 바꾸면 값이나 상태를 갱신할 수 있다.
void handleTabChange() {
if (_tabController.indexIsChanging) return; // --- (5)
widget.onTabChange(_tabController.index);
}
TabController는 리스너뿐 아니라 탭과 관련 콘텐츠를 관리하도록 돕는 게터(getter)를 갖는다.
위 코드와 같이 해당 메서드에서 현재 선택된 탭이 어떤것인지 앱에 알린다.
여기서 중요한 부분은 setState 인데, 선택된 탭에 맞는 화면을 다시 그리도록 명령한다.
TabController.index는 현재 선택된 탭을 가르킨다. TabController는 탭 정보를 가져오고 현재 선택된 탭 정보를 갱신하는 역할만 할 뿐이다.
TabBar 위젯 실습
이제 TabBar 위젯이 구성되는 time_picker_row.dart파일의 build메서드를 살펴보자.
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)
);
}
Widget
1 스타일을 정의하는 TabBar설정 옵션이다.
2. 부모가 위젯에 TabController를 전달한다.
3. ForecastPage에서 tabItems를 전달한다.
4. 기본적으로 탭은 스크롤할 수 없다. 이 프로퍼티를 true로 하면 스크롤할 수 있다.