< Flutter 활용하기 – StatefulWidgets Example >
앞의 글에서 작성한 StatelessWidget class 기반 앱을 수정하여, 사용자와 동적인 반응을 하면서 사용자 인터페이스가 변경되는 형태로 바꾸고자 합니다. 즉, StatefulWidget class 개념을 추가하여 앞서에서의 darttutorial-33-01.dart를 수정 합니다.
Goals of Modification
프로그램의 수정 목표는 단순 합니다. 아래의 [그림1]과 같이 작성한 앞서 글의 프로그램에서, 별표를 클릭하면, [그림2]와 같이 되는 것입니다. 즉, 1) 붉은색인 별모양을 검은색으로 바꿉니다. 2) 별표 옆의 숫자를 1만큼 감소 시킵니다.
만약 별이 검은색인 상태에서 다시 클릭을 하면, 1) 검은색의 별모양을 붉은색으로 바꿉니다. 2) 별표 옆의 숫자를 1만큼 증가 시킵니다. 즉, 누를때 마다 반전(reverse) 작업을 수행하는 단순한 작업을 하도록 개선 합니다.
Code Modification using StatefulWidget
아래의 darttutorial-34-01.dart 프로그램이 수정한 main.dart 프로그램 입니다. 프로그램의 수정은 크게 2 부분에 대해서 이루어 집니다. 첫째는 소스 코드에서 “OLD”로 표기하여 주석 처리한 부분 대신 “New #1″으로 바뀐 부분입니다. 둘째는 “New #2″로 표시한, 프로그램의 후반부에 추가한 사항입니다.
// darttutorial-34-01.dart // Based on : https://flutter.dev/docs/development/ui/layout import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; void main() { var parameter = { 'appBarTitle': 'Flutter Layout Demo Program', 'titleImage': 'images/lake.jpg', 'titleSectionHeader': '경희대학교 국제캠퍼스', 'titleSectionBody': '경기도 용인시 기흥구 덕영대로 1732번지', 'titleSectionScore': 22, 'textSection': '국제캠퍼스는 공학, 응용과학, 생명과학, 전자정보, 소프트웨어, 실용예술, 국제학, ' '외국어, 체육 중심의 캠퍼스로, 특성 있는 학문분야가 많습니다. ' '주요 시설로는 수용 인원 8천여명 규모의 노천극장 및 종합체육관, 천문대, 원자로실 등이 있습니다. ' '경희대는 국제캠퍼스를 포함하여 서울캠퍼스, 광름캠퍼스를 보유하고 있습니다. ' '교명인 경희대(慶熙大)는 영정조 시대의 치세가 펼쳐진 조선시대의 정궁 경희궁(慶熙宮)에서 따온 것으로, ' '임진왜란과 병자호란의 폐허를 딛고 문예를 부흥시킨 조선 후기 영정조 시대처럼, ' '한국 전쟁으로 피폐해진 이 땅에 다시 문화적인 르네상스가 오기를 바라는 마음으로 ' '경희학원(고황재단)의 설립자인 조영식에 의해 명명되었습니다.', }; runApp(MyApp(parameter)); } class MyApp extends StatelessWidget { // Constructor with a Map type input parameter. MyApp(this.internalStorage); // Internal storage to save a page content which initialized by constructor. final Map internalStorage; // build method. @override Widget build(BuildContext context) { Widget titleImage = _buildTitleImage(internalStorage['titleImage']); Widget titleSection = _buildTitleSection( internalStorage['titleSectionHeader'], internalStorage['titleSectionBody'], internalStorage['titleSectionScore']); Widget buttonSection = _buildButtonSection(Theme.of(context).primaryColor); String textSectionMessage = internalStorage['textSection']; Widget textSection = _buildTextSection(textSectionMessage); return MaterialApp( title: internalStorage['appBarTitle'], home: Scaffold( appBar: AppBar( title: Text(internalStorage['appBarTitle']), ), body: ListView( children: [ titleImage, titleSection, buttonSection, textSection, ], ), ), ); } // Build function to create title image Widget dynamic _buildTitleImage(String imageName) { return Image.asset( imageName, width: 600, height: 240, fit: BoxFit.cover, ); } // Build function to create title section Widget Container _buildTitleSection(String name, String addr, int count) { return Container( padding: const EdgeInsets.all(32), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.only(bottom: 8), child: Text( name, style: TextStyle( fontWeight: FontWeight.bold, ), ), ), Text( addr, style: TextStyle( color: Colors.grey[500], ), ), ], ), ), /* OLD Container( child: Row( children: [ Icon( Icons.star, color: Colors.red[500], ), Text('$count'), ], ), ), */ // New #1 : Start Counter(count), // New #1 : End ], ), ); } // Build function to create button section Widget Container _buildButtonSection(Color color) { return Container( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildButtonColumn(color, Icons.call, '전화'), _buildButtonColumn(color, Icons.near_me, '경로'), _buildButtonColumn(color, Icons.share, '공유'), ], ), ); } // Build function to create button with icon and sub-title for _buildButtonSection() Column _buildButtonColumn(Color color, IconData icon, String label) { return Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, color: color), Container( margin: const EdgeInsets.only(top: 8), child: Text( label, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w400, color: color, ), ), ), ], ); } // Build function to create text description section Widget Container _buildTextSection(String section) { return Container( padding: const EdgeInsets.all(32), child: Text( section, softWrap: true, ), ); } } // New #2 : Start class Counter extends StatefulWidget { final _counter; Counter(this._counter) { } @override _CounterState createState() => _CounterState(_counter); } class _CounterState extends State<Counter> { int _counter; bool _boolStatus = true; Color _statusColor = Colors.black; _CounterState(var counterValue) { _counter = counterValue; } void _buttnPressed() { setState(() { if(_boolStatus == true) { _boolStatus = false; _counter--; _statusColor = Colors.black; } else { _boolStatus = true; _counter++; _statusColor = Colors.red; } }); } @override Widget build(BuildContext context) { return Container( child: Row( children: [ IconButton( icon: Icon(Icons.star), color: _statusColor, onPressed: _buttnPressed, ), Text('$_counter'), ], ), ); } } // New #2 : End
수정사항 #1은 앞서 darttutorial-33-01.dart에서 고정적인 형태로 만든 붉은색 별표 아이콘과 고정된 count 값을, count 값을 constructor의 입력 파라메타로 대체한 Counter class의 객체로 교체한 것 입니다.
수정사항 #2는 이제 정적인 별모양과 count 값이, 동적으로 변경될 것이니, 이를 저장하고 관리할 StatefulWidget과 이 widget이 다룰 State<> class를 만드는 작업입니다. 먼저, 앞서 #1에서 제거한 모양을 대체하는 모양이 _CounterState class 안의 build() 속에 포함된 것을 볼 수 있습니다. build() 함수의 기능은 이미 여러번 언급이 되었듯이, 화면에 사용자 인터페이스를 표시하는 역할을 합니다. 이 함수가 리턴하는 Container() 객체의 구성 요소를 살펴보면, 앞서 제거한 고정 형태의 Container()와 동일하게 별모양 아이콘과 Text로 이루어진 것을 알수 있습니다. 단, 고정 방식에서는 color가 붉은 색으로 고정되었으나, 수정한 코드에서는 stateColor 변수의 값에 따라 달라지도록 하였습니다. 고정 방식에서는 없었던 onPressed method를 채우는 부분이 생겼으며, 이는 별모양을 클릭하면 buttonPressed() 함수가 호출되는 것을 의미합니다. 별 모양 옆에 같은 Row로 표시되는 글자는 마찬가지로 counter라는 변수 값을 표시하도록 되어 있습니다.
새로 등장한 사항을 확인해 보면, 먼저 statusColor는 CounterState class의 property로 만들어져 있는 것을 볼 수 있습니다. 다음으로 buttnPressed() method가 CounterState class 안에 정의되어 있는 것을 볼수 있습니다. 여기서, buttnPressed() 안을 보면, build() 함수를 trigger하여 화면을 업데이트 하는 setState()가 실행되는데, 여기에 전달하는 함수의 기능을 살펴보면, boolStatus 값이 true/false의 이분법적인 상태에 맞춰서, 별모양의 색을 붉은색과 검정색 사이에서 변경하고, counter의 값을 1만큼 늘리거나 줄이는 작업을 하는 것을 알 수 있습니다. 따라서, 단추를 눌렀을때 모양과 숫자가 바뀌는 이유를 이제 이해할 수 있을 겁니다. 별모양 옆의 숫자는 다음의 순서를 거쳐서 CounterState class의 counter 값으로 전달됩니다. 먼저 MyApp class의 buildTitleSection()이 실행하면서, Counter 객체를 만들게 되고, 여기서 count 값을 초기화 합니다. 이를 받은 Counter 클래스의 객체는 자신의 property를 받은 count 값으로 초기화 한 후, 관리할 State인 CounterState class 객체의 초기화 값으로 전달하게 되고, CounterState class는 이를 내부 property인 counter에 저장함으로, 프로그램 시작시의 초기화 값을 전달 받게 됩니다.
마무리
지금까지 Flutter의 StatelessWidget과 StatefulWidget을 이용하여, 정적인 사용자 인터페이스와 동적인 사용자 인터페이스를 구현해 보았습니다. 지금까지 실행해 온 프로그램들의 소스 코드를 살펴보면 알겠지만, Flutter는 간단하게 동작하는 Widget들을 활용하여 단순한 화면을 구성하거나, Widget들을 복합적으로 사용하여, 복잡한 기능과 표현을 구현할 수 있습니다. 이러한 이유로 Flutter를 시작할 때, 다양한 Reference들을 먼저 소개하였습니다. 어떤 Widget들을 Flutter가 지원하는지와 3rd party를 통해서 제공되는 Widget들은 어떤 것들이 있는지를 이해한다면 프로그램을 개발하는데 큰 도움이 될 겁니다.
Creative Commons License (CC BY-NC-ND)