Layout : 자주 사용하는 UI 위젯

Clean Code Big Poo·2022년 5월 9일
0

Flutter

목록 보기
9/38
post-thumbnail

소스를 올리고 싶은데... flutter in action에서 제공하는 소스코드가 nullsafe 이전 버전인데다 상속받아 만든 위젯이 너무 많다... 가져다 쓰다가 피보는 중 ㅜ
처음부터 다시 만드는게 덜 고생스러울듯... 일단 스탑!
flutter in action github

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),
      ],
    ),
  ],
),
  1. Stack은 Row나 Column처럼 여러 자식(children)을 갖는다.
  2. 배경에 태양과 달을 그리는 위젯 position 프로퍼티를 포함한다.
  3. position 프로퍼티는 위젯의 위치를 명시적으로 지정한다는 점에서 Positioned.position과 비슷하지만 SlideTransition의 Poisitioned 프로퍼티는 바꿀 수 있는 값을 갖는다는 점이 다르다.
  4. Stack의 두 번째 자식은 Clouds이며 이 위젯은 태양 위에 그려진다.
  5. Stack의 최상위 레이어의 콘텐츠다.

Table

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

🧨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의 모든 행은 같은 수의 자식을 가져야 한다.
  • 자식의 서브 위젯 트리에 TableCell을 반드시 사용할 필요는 없다.
    TableCell은 TableRow의 직계 자식이어야 할 필요는 없으며 위젯 트리의 어딘가에 TableRow를 조상으로 가지면 충분하다.

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(...),
        ...
        ),
      ],
    );
  }),
),
  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),
  ],
),
  1. forecast 페이지의 모든 컨텐츠를 포함한다.
  2. 첫 번째 자식 위젯이 바닥에 위치하도록 열의 순서를 역전시킨다.
  3. Table 위젯을 가리키는 변수이다.
  4. 날씨 앱의 추가 위젯이다.

TabBar

  • 내장 TabBar 위젯의 자식들은 스크롤할 수도 있고, 수평의 뷰로 구성되며 선택할 수 있는(tappable)기능을 갖는다.

  • 탭 바의 위젯을 탭하면 탭 바 위젯에 전달한 콜백이 호출된다.

    • 이 콜백을 이용해 탭 바의 자식 위젯은 페이지의 위젯을 바꾼다.

  • 위 사진에서는 사용자가 선택할 수 있도록 시간을 표시하는 자식 위젯과 필요한 기능을 처리하는 TabController, 이 두가지 핵심 기능을 포함한다.

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)
	...
}
  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);
  }

	//...
}
  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);
}

TabController는 리스너뿐 아니라 탭과 관련 콘텐츠를 관리하도록 돕는 게터(getter)를 갖는다.

위 코드와 같이 해당 메서드에서 현재 선택된 탭이 어떤것인지 앱에 알린다.
여기서 중요한 부분은 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)
  );
}

1 스타일을 정의하는 TabBar설정 옵션이다.
2. 부모가 위젯에 TabController를 전달한다.
3. ForecastPage에서 tabItems를 전달한다.
4. 기본적으로 탭은 스크롤할 수 없다. 이 프로퍼티를 true로 하면 스크롤할 수 있다.

TabBar 요점

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

0개의 댓글