Dart Programmer 되기 [32]

< Flutter 활용하기 – Understanding Widgets >

이제 Flutter에서 새롭게 프로젝트를 만드는 경우에 자동으로 만들어지는 Start App에 대해서 알아보도록 하겠습니다.

Flutter Start App의 이해

아래의 darttutorial-32-01.dart 프로그램이 Start App 프로그램에서 주석문(comment)만 제거한 프로그램 입니다.

[프로그램] darttutorial-32-01.dart (Flutter Start App)

3~9번 라인은 앞서 글의 내용과 동일합니다.

10번 라인에서 MaterialApp class의 객체를 만들어서 return 하는 것을 볼수 있습니다. Material App은 Android 운영체제를 위한 Material 디자인을 지원하는 App을 의미합니다. (참조: https://api.flutter.dev/flutter/material/MaterialApp-class.html )

MaterialApp class에서 지원하는 property중 title은 사용자에게 App을 식별하기 위한 한줄의 표현 입니다. 이 경우 “Flutter Demo”라는 문자열 입니다. 이 프로그램이 디바이스에 설치되면 이 이름이 앱의 이름으로 사용됩니다. theme는 해당 App의 material widget(들)에 대한 시각적 속성을 표현합니다. 이때 primarySwatch는 MaterialColor라고 하며, material app가 사용한 색의 different shade를 정의합니다. 이 경우 파랑색(Colors.blue)임을 알수 있습니다.

이를 확인하기 위해서 앞서에서 처럼 새로운 프로젝트를 만들고, Start App을 바로 실행하면, 기본 설정인 파랑색으로 배경이 채워지는 것을 아래의 실행화면 [그림 0]와 같이 볼 수 있습니다. (이를 확인하기 위하여, 이 프로그램을 실행합니다. 그리고 Colors.blue를 Colors.red로 바꿔서 App의 상단부 색깔이 바뀌는 것을 확인해 봅니다) home은 App의 default route(기본 페이지 화면)로 사용할 widget을 나타내며, 20번 라인을 보면, StatefulWidget을 확장한 MyHomePage의 객체임을 알 수 있습니다. 이의 이름은 ‘Flutter Demo Home Page’으로 정의하는 것을 볼 수 있습니다. 결국 이 프로그램의 주요 동작과 모양은 MyHomePage class에서 정의되는 것을 알수 있습니다.

[그림 0] Start App (darttutorial-32-01.dart) 실행 화면

MyHomePage class는 20~26번 라인에서 정의합니다. MyHomePage는 StatefulWidget을 확장하는데, 이는 mutable한 값을 저장하고 관리하는 경우에 사용하는 class 입니다. 22번 라인에서 title 변수가 있고, 15번 라인에서 title을 ‘Flutter Demo Home Page’의 문자열로 전달 받은후, 21번 라인에서 처럼, title 변수에 이 값을 저장하는 것을 볼 수 있습니다. MyHomePage class는 createState() method가 있으며, 이를 통해서 MyHomePageState class의 객체가 만들어 지는 것을 알 수 있습니다. 그리고 이 class 안에서는, StatefulWidget이 다룰 State class를 생성하는 createState()가 호출됩니다. 따라서 실질적인 프로그램의 작업은 mutable한 정보를 저장/관리하는 MyHomePageState class안에서 이루어 지게 됩니다.

여기까지의 내용을 단계적으로 표현하면 1) Dart의 main()이 실행 됩니다. 2) Flutter의 main()에 해당하는 runApp()을 실행합니다. 3) 프로그램을 디바이스에 등록하고, 기본적인 GUI 환경을 설정하는 (StatelessWidget를 확장한) MyApp class를 만들고, build 함수를 override 하여, 화면이 나타나도록 합니다. 이 경우 MaterialApp 기반의 GUI 디자인을 사용합니다. 4) mutable한 정보를 다룰 필요가 있는 경우 StatefulWidget인 MyHomePage class를 만들어서 3)의 widget에 포함하여 생성하도록 합니다. 그리고 createState()를 override해서 mutable한 정보를 저장하고 관리하는 State<> class를 생성하도록 합니다. 5) StatefulWidget이 생성/관리할 정보를 다룰 class를 State<> class를 확장하여 MyHomePageState class로 생성합니다.

이제 마지막으로 남은, MyHomePageState class안의 설명을 합니다. 이에 대해서는 화면에 나타난 GUI와 기능을 설명하는 방식으로 하겠습니다. 먼저, 앞서의 그림은 3단으로 나뉩니다. 맨위의 파랑 바탕에 타이틀인 ‘Flutter Demo Home Page’이 있는 부분, 중간에 “You have pushed the button this many times:”과 숫자”0″이 있는 부분, 그리로 하단의 “+” 기호가 있는 둥근 원 모양의 단추 입니다. 이런 스타일의 GUI Layout은 39번 라인에서 만들어지는 Scaffold widget을 통해서 만들어 집니다. Scaffold widget은 Material 디자인의 시각적 레이아웃 구조를 나타내며, drawers, snack bars 그리고 bottom sheets 등의 다양한 요소를 포함할 수 있습니다. (참조: https://api.flutter.dev/flutter/material/Scaffold-class.html )

MyHomePageState class의 Scarffold class는 화면의 상단 부분을 표현하기 위해서 AppBar 클래스 타입으로 Scarffold의 appBar property를 채웁니다. AppBar는 MyApp class의 생성자에 부여한 title로 초기화 됩니다. 중간 부분은 앞서의 글에서도 등장했었던 Center class로 채워집니다. mainAxisAlignment를MainAxisAlignment.center로 하여 화면의 중간에 위치하도록 하였으며, Text class 두개를 Widget list에 담아서 Center class의 children property를 채웁니다. 이를 통해서 Center class에 두개의 문자열이 있도록 하는 것 입니다. 마지막으로 하단의 버튼은 FloatingActionButton class를 만들어서 Center class의 floatingActionButton을 채워서 만듭니다. 이 class는 child property를 통해서 더하기 기호에 해당하는 Icons.add 아이콘을 가지며, 클릭이 되면(onPressed) incrementCounter method가 호출되도록 합니다. 그렇다면 이 method는 어디 있나요? MyHomePageState class의 처음 부분으로 올라가면, 이 method가 만들어져 있습니다. 실행이 되면, setState()를 실행하는데, 입력 파라메타로 이름이 없는 함수를 주며, 이 함수가 counter 값을 증가시키고, 이 변수는 MyHomePageState class 안의 property로 29번 라인에서 만들어져 있음을 알 수 있습니다. 따라서 화면의 하단 단추를 누르면, counter 변수 값을 증가시키게 되는 것을 알 수 있습니다. 그렇다면, 이렇게 증가된 값이 어떻게 Center class안의 children 안의 두번째 Text를 통해서 화면에 갱신되어 출력이 되는 걸까요? 이에 대한 해답을 찾기 위해서 다소 복잡하지만 반드시 알아야 하는 “build()와 setState()의 상관 관계를 이해”라는 여정을 떠나야 합니다.

Deep Dive into Flutter Widget Operation

Stateless Widget은 immutable한 정보를 갖는 Widget 이지만, Stateful Widget은 State 정보를 가지고 mutable하게 관리 합니다. 이에 대해서, 간단하게 [그림 1]에 도식화 되어 있습니다. 만약 그래픽 인터페이스와 연계한다면, Stateless Widget은 절대로 바뀌지 않는 그래픽 인터페이스이며, Stateful Widget은 내부 정보에 따라서 그래픽이 달라지는 형태라도 볼 수 있습니다.

Stateless Widget은 여러 widget 들을 모아서 사용자 인터페이스를 구성할 때 주로 사용합니다. 이 경우 build()의 진행은 회귀적으로(recursively) 모든 사항이 concrete한 수준이 될 때까지 진행되는데, 즉 concrete하게 모든 widget들이 RederObjects/RenderObjectWidgets가 될 때까지 진행합니다. 따라서 Stateless Widget은 사용자 인터페이스를 구성하는 경우, 오로지 고정된 configuration 정보 만으로 구성하는 경우들에 대해서 유용합니다.

Stateful Widget은 반대의 경우로 사용자 인터페이스를 구성하는 widget 들의 집합/위치/형태 등이 동적이고, 상시 변할 수 있는 경우에 유용합니다. 시간이나 이벤트에 따라서 상태가 변하는 경우들에서 유용합니다. 그리고 동적으로 변하는 속성상, Widget이 build 되고 있을 때에서는 동기적으로 데이터를 읽을 수 없습니다.

[그림 1] Stateless Widget과 Stateful Widger의 개념
(출처) https://medium.com/@rkishan516/flutter-book-stateless-and-stateful-widgets-8db946f45df5

StatelessWidget을 extends한 Stateless Widget Class 객체는, constructor를 통해서 전달 받은 정보를 final 멤버 변수로 저장합니다. 이 정보들은 build()를 실행하면서 사용합니다. Stateless Widget의 멤버 변수를 바꿀 필요가 있다면, Stateless Widget은 이를 직접 바꾸지 않고, (즉, Widget 내부의 멤버 변수의 값을 바꾸는 것이 아니고, 대부분의 경우) parent widget으로 부터 전달 받은 함수를 호출하게 됩니다. 예를 들어, Stateless Widget이 flag 값을 가지고 있고, true/false 이라면, parent 함수를 호출해서, flag 값이 바뀐 새로운 Stateless Widget을 만들어서 parent에 등록 하는 형태입니다. 이렇게 하여, 프로그램에서 관리해야 하는 정보가 Stateless Widget를 떠나서 parent widget 레벨에서 관리가 됩니다. 이런 방식으로 (Stateless Widget의 수명이 아닌) 프로그램에서 필요로 하는 기간 동안 정보를 관리하는 것이 가능합니다. 극단적으로 프로그램의 수행부터 종료까지 정보를 관리하는 것도 가능합니다.

앞서의 예처럼, Stateless Widget이 parent에서 전달한 call back 함수를 호출하면, parent는 자신의 내부 상태를 업데이트하고, 새로운 Stateless Widget을 (변경한 정보를 갖는 상태로) rebuild/create 합니다. 새로운 Stateless Widget들을 (상태 변경이 필요할 때마다) parent가 새롭게 생성하는 부분에서 성능 저하를 우려할 수 있습니다. 이와 관련해서, Flutter는 새롭게 만드는 Widget과 기존의 Widget을 비교하여, 대부분은 재사용을 하고, 단지 차이가 있는 부분을 RenderObject에서 처리하여, 성능 상의 부하를 줄이고 있습니다.

Flutter에서 widget 들은 tree 형태로 관리 됩니다. 예를 들어, Flutter 공식 사이트의 예제를 기반으로 설명할때, 다음과 같은 그래픽 인터페이스를 만든다고 가정해 봅니다. (출처: https://flutter.dev/docs/development/ui/layout)

Flutter에서는 위의 구조를 프로그래밍 할 때 다음의 그림처럼 여러 구성 요소의 집합으로 구현할 수 있습니다.

즉, 3개의 icon과 이들 각각에 대한 글자(text)가 보입니다. 따라서 icon과 text를 묶는 논리적인 그룹을 생각할 수 있는데, 이들은 위/아래의 수직적인 그룹(column)입니다. 이 그룹이 3개 수평적으로 그룹(row)을 만들고 있습니다. 그리고 이 그룹은 하나의 큰 박스(container) 안에 담아서 처리합니다. 이를 tree 형태로 도식화 하면 다음과 같습니다. Row, Column, Icon, Text는 직관적으로 이해가 가능하지만, Container는 일부 설명이 필요해 보입니다. Container는 내부에 포함하는 child widget들을 customize하는 용도로 사용합니다. 즉, padding, margin, border, background color, icon color, text style 등을 조정하기 위한 목적으로 사용합니다. 따라서 아래의 tree 구조를 살펴보면, Row와 Text에 대한 customization을 위해서 Container를 사용하여 감싸는 것을 유추할 수 있습니다.

[출처] https://flutter.dev/docs/development/ui/layout

StatefulWidget을 extends한 Stateful Widget Class 객체는 mutable state를 저장합니다. 이런 Stateful Widget이 tree안에 삽입되면, Flutter framework는 createState() 함수를 호출하여 Stateful Widget 객체가 관리할 State Class 객체들을 새롭게 만들고, tree에 이들의 위치 정보도 함께 저장합니다. State Class를 extends한 서브 클래스 들은 private 속성을 가지기에, 이름을 “_”로 시작합니다. 만약 Stateful Widget의 parent가 rebuild를 실시하면, parent는 하위 체계의 Stateful Widget의 객체를 새롭게 만듭니다. 하지만 Flutter framework는 기존의 Stateful Widget 객체가 이미 저장하고 있던 State Class 객체를 재사용하여, Stateful Widget Class의 createState() 함수가 다시 호출되는 것을 지양합니다.

Stateful Widget이 관리하는 State Class 객체에서, Stateful Widget의 property에 접근하고자 한다면, State Class 객체는 자신의 widget Property를 사용할 수 있습니다. 만약 Stateful Widget의 parent가 Stateful Widget 객체를 rebuild하고 새롭게 만들고자 한다면, Stateful Widget이 관리하는 State Class 객체는 자신의 widget Property를 새롭게 만들어진 widget Property로 rebuild 됩니다. 이런 경우, 개발자가 widget Property의 변경 여부를 직접 받아보고 싶다면, didUpdateWidget() 멤버 메소드를 override 합니다. 이 메소드를 사용하면, 현재의 widget과 과거의 widget을 비교할 수 있습니다.

State Class 객체의 상태를 변경하는 함수 혹은 call back 함수가 실행되면, Flutter framework에 내부 상태가 변경 되었음을 알리기 위해서, setStaste() 함수에 필요한 작업을 작성하여 실행합니다. setStaste() 함수를 호출한다는 의미는, Flutter framework에게 상태 변화가 있음을 알려줘서, 예정된 다음 차례의 프로그램 화면 업데이트 시점에서, 반영해 달라(build() 함수를 호출해 달라)는 의미입니다. setStaste() 함수 호출을 개발자가 누락하면, Flutter framework는 상태의 변화를 알지 못하게 되며, build() 함수 호출을 하지 않게 됩니다. 이렇게 state를 관리 함으로써, Widget의 복잡한 tree 체계를 개발자가 모두 이해해서, child widget들의 생성과 업데이트를 일일이 처리하지 않아도 됩니다. 단지 개발자는 build() 함수를 만들면, 이 모든 사항을 Flutter framework가 처리합니다.

StatefulWidget에서 createState ()를 호출 한 후, Flutter 프레임 워크는 새로 만든 State 객체를 트리에 삽입 한 다음 상태 객체에서 initState()를 호출합니다. State의 서브 클래스는 initState()를 override하여 한 번만 수행해야 하는 작업을 수행 할 수 있습니다. 예를 들어 애니메이션을 구성하거나 플랫폼 서비스를 구독하려면 initState()를 override 합니다. 이 경우에는, override하는 initState() 함수의 시작 부분에서 super.initState()를 호출하여야 하여야 합니다.

State 객체가 더 이상 필요하지 않으면 Flutter 프레임 워크가 해당 State 객체의 dispose()를 호출합니다. 필요한 정리 작업이 있다면, dispose()를 override 하도록 합니다. 예를 들어 타이머를 취소하거나 플랫폼 서비스의 구독을 취소하려면 dispose()를 override 합니다. dispose()의 구현은 일반적으로 super.dispose()를 함수의 마지막 부분에서 호출하여 종료합니다.

StatefulWidget의 life cycle과 함수들의 관계를 그림으로 설명하면 다음의 [그림 2]와 같습니다.

[그림 2] Stateful Widget과 State의 상관 관계
(출처: https://livebook.manning.com/book/flutter-in-action/chapter-8/v-9/37)

StatefulWidget의 life cycle에 연관된 함수들을 간단히 설명하면 다음과 같습니다. (참조: https://stackoverflow.com/questions/47501710/what-is-the-relation-between-stateful-and-stateless-widgets-in-flutter/49377090#49377090)

createState()는 Flutter가 StatefulWidget을 만들 때 호출됩니다. 이 함수는 tree안의 주어진 위치에 StatefuleWidget을 위한 mutable한 state를 생성합니다. 서브 클래스는 반드시, 연관된 State 서브 클래스를 기반으로 해서, 새롭게 만들어진 객체를 리턴 하도록 이 함수를 override 해야 합니다.

@override
_MyState createState() => _MyState();

initState()는 widget이 생성될 때, constructor 다음으로, 가장 먼저 호출되는 메소드 입니다. 이 함수는 반드시 한번만 실행되며, super.initState() 함수를 실행해야 합니다. 생성하는 widget 객체에 대한 특정 BuildContext에 연관된 데이터를 초기화 합니다. 그리고 tree 구조에서 (생성하는 widget 객체의) parent widget들에 연관된 property들도 초기화 합니다. 마지막으로 ChangeNotifiers 스트림에 가입하거나 혹은 생성하는 widget의 데이터를 변경할 수 있는 다른 객체에 가입합니다.

@override
 initState() {
   super.initState();
   // Add listeners to this class
   cartItemStream.listen((data) {
     _updateWidget(data);
   });
 }

build()는 widget에 의하여 표현되는 사용자 인터페이스 부분을 표현합니다. Flutter framework는 다음의 같은 상황에서 이 함수를 호출합니다. 1) initState() 함수를 호출한 다음, 2) didUpdateWidget() 함수를 호출한 다음, 3) setState() 함수의 호출을 요청 받은 경우, 4) 이 State 객체의 dependency가 변경되는 경우 (즉, 이전의 build에 의해서 참조하는 InheritedWidget이 변경되는 경우), 5) Deactivate를 호출한 후, State 객체를 tree 구조의 다른 위치로 재 삽입한 경우 입니다. Flutter framework는 이 widget 하단의 subtree를 build() 메소드의 리턴 값으로 교체하거나, 기존 subtree를 업데이트 하거나, 혹은 subtree를 삭제하고 새로운 subtree로 대체합니다. 이때 build()가 리턴하는 widget이 기존 subtree의 root를 업데이트 할지 여부를 결정하는 인자가 되며, 이에 대한 정보는 Widget.canUpdate를 호출하여 알 수 있습니다. 일반적으로 build()의 implementation들은, a) widget의 constructor에서 받은 정보, b) 주어진 BuildContext 그리고 c) State 객체의 내부 상태 정보를 기반으로 구성된 ‘widget들의 집합으로 만들어진 객체’를 리턴 합니다.

@override
Widget build(BuildContext context, MyButtonState state) {
  … () { print("color: $color"); } …
}

setState()에는 State 객체의 내부 상태가 변경될 때 마다, 반영해야 하는 사항을 구현 합니다. setState()를 호출한다는 것은 Flutter framework에게 이 객체의 내부 상태가 바뀌었다는 것을 알려주는 것으로써, 이 subtree안의 사용자 인터페이스에 영향이 있을 수 있다는 의미입니다. 따라서, Flutter framework는 다음 업데이트 시점에서 이 State 객체에 대한 build를 수행하게 됩니다.

setState(() { _myState = newValue });

didUpdateWidget()는 widget의 구성(configuration)이 바뀔 때 마다 호출됩니다. 만약 parent widget이 rebuild를 수행하고 tree 업데이트를 요청해서, 이 위치에 있는 (동일 runtime type과 Widget.key를 갖는) 새로운 widget을 디스플레이 하도록 요청하면, Flutter framework는 State 객체의 widget property가 새로운 widget을 참조하도록 업데이트 한 후, 기존의 widget을 argument로 해서 didUpdateWidget() 메소드를 호출합니다. 따라서, widget의 변경에 대해서 반응하고 싶다면 이 메소드를 override 합니다. Flutter framework는 didUpdateWidget() 메소드를 호출한 후에는 항상 build를 실행 합니다. 따라서, didUpdateWidget() 메소드에서의 setState()는 필요하지 않습니다.

@mustCallSuper
@protected
void didUpdateWidget(covariant T oldWidget) { }

dispose()는 tree에서 객체가 제거될 때 호출됩니다. Flutter framework는 State 객체가 다시는 build 되지 않는 다고 판단되면 이 메소드를 호출합니다. 이 메소드를 실행하면, State 객체는 unmout되고 mounted Property는 flase 값을 가집니다. 이후로 이 객체에 대한 setState()는 에러를 유발합니다. 한번 dispose된 객체는 다시 사용(mount)할 수 없습니다. 서브 클래스에서는 점유했던 자원을 해제하거나, 활성화한 애니메이션을 중단하는 등의 작업을 수행합니다.

@protected
@mustCallSuper
void dispose() {
  assert(_debugLifecycleState == _StateLifecycle.ready);
  assert(() { _debugLifecycleState = _StateLifecycle.defunct; return true; }());
}

Flutter 사이트에서 제시하는 Stateless Widget의 예제가 아래의 Frog Class로 입니다. Frog 클래스는 내부적으로 color와 child 멤버 변수를 final 타입으로 가집니다. 그리고, contructor를 통해서 이들의 값과, Widget의 key 값을 받습니다. 그리고 build 함수를 통해서 Container 형태에 멤버 변수를 표현하도록 하고 있습니다.

// Reference: https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html
 class Frog extends StatelessWidget {
   const Frog({
     Key key,
     this.color = const Color(0xFF2DBD3A),
     this.child,
   }) : super(key: key);
 final Color color;
   final Widget child;
 @override
   Widget build(BuildContext context) {
     return Container(color: color, child: child);
   }
 }

Flutter 사이트에서 제시하는 Stateful Widget의 예제가 아래의 Bird Class 입니다. Stateful Widget도 immutable 하므로 final 타입의 color와 child 멤버 변수를 가집니다. 그리고 State를 보관하기 위하여 createState() 멤소드를 가집니다. 그리고 이 메소드는 State의 서브 클래스인 BirdState 객체를 리턴하며, private 타입이기에 “_”로 이름이 시작합니다. _BirdState 클래스는 state 정보로 _size를 가집니다. 이를 증가하는 grow() 메서드를 가지고 있으며, build() 메소드를 통해서, Container 타입으로 color와 child, 그리고 size를 기반으로 하는 값을 표현합니다.

// Reference: https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html

 class Bird extends StatefulWidget {
   const Bird({
     Key key,
     this.color = const Color(0xFFFFE306),
     this.child,
   }) : super(key: key);
 final Color color;
   final Widget child;
 _BirdState createState() => _BirdState();
 }

 class _BirdState extends State {
   double _size = 1.0;
 void grow() {
     setState(() { _size += 0.1; });
   }
 @override
   Widget build(BuildContext context) {
     return Container(
       color: widget.color,
       transform: Matrix4.diagonal3Values(_size, _size, 1.0),
       child: widget.child,
     );
   }
 }

Flutter 공식 사이트에서 보다 자세한 설명을 얻을 수 있습니다. 다음은 앞서 설명한 클래스들과 주요 메소드에 대한 공식 사이트 링크들이니, 필요한 경우 참조하기 바랍니다.

  • StatelessWidget Class : https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html
  • StatefulWidget Class : https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html
  • State Class : https://api.flutter.dev/flutter/widgets/State-class.html
  • build method in State Class : https://api.flutter.dev/flutter/widgets/State/build.html
  • Comments for Android : https://flutter.dev/docs/get-started/flutter-for/android-devs
  • Comments for iOS : https://flutter.dev/docs/get-started/flutter-for/ios-devs

Sample Programs

앞서 설명한 내용의 이해를 토대로 Flutter 사이트에서 제공하는 몇개의 sample 프로그램을 이해하는 과정을 거쳤으면 합니다. 본인이 무언가를 만들기 전, 다른 이가 만든 예제를 읽어서 모방하는 것은 매우 중요한 과정이니, 앞서의 내용에 대한 이래를 토대로 다음의 Flutter 예제들을 하나 하나 읽어보고, 직접 실행해서 이래가 맞는지 확인해 보기 바랍니다.

Sample Program 1 – Code & Screen Shot

// darttutorial-32-02.dart : Flutter HelloWorld using own widgets 
// Reference:https://flutter.dev/docs/development/ui/widgets-intro#basic-widgets
 import 'package:flutter/material.dart';
 void main() => runApp(MyApp());
 class MyAppBar extends StatelessWidget {
   MyAppBar({this.title});
 final Widget title;
 @override
   Widget build(BuildContext context) {
     return Container(
       height: 56.0, 
       padding: const EdgeInsets.symmetric(horizontal: 8.0),
       decoration: BoxDecoration(color: Colors.blue[500]),
       child: Row(
         children: [
           IconButton(
             icon: Icon(Icons.menu),
             tooltip: 'Navigation menu',
             onPressed: null, 
           ),
           Expanded(
             child: title,
           ),
           IconButton(
             icon: Icon(Icons.search),
             tooltip: 'Search',
             onPressed: null,
           ),
         ],
       ),
     );
   }
 }
 class MyScaffold extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return Material(
       child: Column(
         children: [
           MyAppBar(
             title: Text(
               'Example title',
               style: Theme.of(context).primaryTextTheme.title,
             ),
           ),
           Expanded(
             child: Center(
               child: Text('Hello, world!'),
             ),
           ),
         ],
       ),
     );
   }
 }
 class MyApp extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return MaterialApp(
       title: 'My app',
       home: MyScaffold(),
     );
   }
 }
[그림] darttutorial-32-02.dart 실행 화면

Sample Program 2 – Code & Screen Shot

// darttutorial-32-03.dart : Flutter HelloWorld using baseline widgets 
// Reference: https://flutter.dev/docs/development/ui/widgets-intro#using-material-components
 import 'package:flutter/material.dart';
 void main() => runApp(MyApp());
 class MyApp extends StatelessWidget {
   @override 
   Widget build(BuildContext context) {
     return MaterialApp(
       title: 'darttutorial-32-03.dart',
       home: MobileAppHome(),
     );
   }
 }
 class MobileAppHome extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     // Scaffold is a layout for the major Material Components.
     return Scaffold(
       appBar: AppBar(
         leading: IconButton(
           icon: Icon(Icons.menu),
           tooltip: 'Navigation menu',
           onPressed: null,
         ),
         title: Text('Example title'),
         actions: [
           IconButton(
             icon: Icon(Icons.search),
             tooltip: 'Search',
             onPressed: null,
           ),
         ],
       ),
       // body is the majority of the screen.
       body: Center(
         child: Text('Hello, world!'),
       ),
       floatingActionButton: FloatingActionButton(
         tooltip: 'Add', // used by assistive technologies
         child: Icon(Icons.add),
         onPressed: null,
       ),
     );
   }
 }
[그림] darttutorial-32-03.dart 실행 화면

Sample Program 3 – Code & Screen Shot

// darttutorial-32-04.dart : Product order program
// Reference: https://flutter.dev/docs/development/ui/widgets-intro#bringing-it-all-together

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Shopping App',
      home: ShoppingList(
        products: <Product>[
          Product(name: 'Eggs'),
          Product(name: 'Flour'),
          Product(name: 'Chocolate chips'),
        ],
      ),
    );
  }
}

class Product {
  const Product({this.name});
  final String name;
}

typedef void CartChangedCallback(Product product, bool inCart);

class ShoppingListItem extends StatelessWidget {
  ShoppingListItem({this.product, this.inCart, this.onCartChanged})
      : super(key: ObjectKey(product));

  final Product product;
  final bool inCart;
  final CartChangedCallback onCartChanged;

  Color _getColor(BuildContext context) {
    return inCart ? Colors.black54 : Theme.of(context).primaryColor;
  }

  TextStyle _getTextStyle(BuildContext context) {
    if (!inCart) return null;

    return TextStyle(
      color: Colors.black54,
      decoration: TextDecoration.lineThrough,
    );
  }

  @override
  Widget build(BuildContext context) {
    return ListTile(
      onTap: () {
        onCartChanged(product, inCart);
      },
      leading: CircleAvatar(
        backgroundColor: _getColor(context),
        child: Text(product.name[0]),
      ),
      title: Text(product.name, style: _getTextStyle(context)),
    );
  }
}

class ShoppingList extends StatefulWidget {
  ShoppingList({Key key, this.products}) : super(key: key);

  final List<Product> products;

  @override
  _ShoppingListState createState() => _ShoppingListState();
}

class _ShoppingListState extends State<ShoppingList> {
  Set<Product> _shoppingCart = Set<Product>();

  void _handleCartChanged(Product product, bool inCart) {
    setState(() {
      if (!inCart)
        _shoppingCart.add(product);
      else
        _shoppingCart.remove(product);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Shopping List'),
      ),
      body: ListView(
        padding: EdgeInsets.symmetric(vertical: 8.0),
        children: widget.products.map((Product product) {
          return ShoppingListItem(
            product: product,
            inCart: _shoppingCart.contains(product),
            onCartChanged: _handleCartChanged,
          );
        }).toList(),
      ),
    );
  }
}

[그림] darttutorial-32-04.dart 실행 화면

마무리

다소 복잡한 내용에 대해서 배웠습니다. 하지만 이 글에서 다룬 내용은 Flutter의 GUI가 동작하는 부분에서 주요한 사항이니 당장은 이해가 안가더라도 틈틈히 읽어서 이해를 넓혀갔으면 좋겠습니다. 아울러 흔히 Flutter를 설명할 때, “Widget is Everything”이라고 하는데, 다양한 Widger을 보고 경험하여 응용한다면, 많은 공을 들이지 않더라도 제법 모양을 갖춘 프로그램의 개발이 가능할 것 입니다.

Creative Commons License (CC BY-NC-ND)

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다