Flutter의 위젯에 대해 간단히 다루고 있습니다.
2022. 7. 7 최초작성
다음 글을 바탕으로 작성되었습니다.
https://docs.flutter.dev/development/ui/widgets-intro
본 포스트를 읽어보기 전에 다음 포스트에서 StatefulWidget과 StatelessWidget의 차이를 확인하세요.
Flutter 강좌 02 - Flutter에서 StatefulWidget과 StatelessWidget의 차이 정리
https://webnautes.tistory.com/2061
관련 포스트
MacBook M1에 iOS와 Android를 위한 Flutter 개발 환경 만들기
https://webnautes.tistory.com/2027
Windows에서 Flutter 개발환경 만들기
https://webnautes.tistory.com/2057
플러터는 리액트(React)에서 영감을 받아 만들어진 프레임워크(framework)입니다. 위젯(widget)을 트리(tree) 구조 형태로 결합하여 유저 인터페이스(UI)를 만듭니다. 위젯은 구성(Configuration) 및 상태(State)가 주어지면 뷰(View)가 어떤 모습으로 보여야 하는지를 기술(description)합니다. 위젯의 상태가 변경되면 위젯은 해당 위젯에 대한 기술을 다시 작성하고 프레임워크는 기본 렌더(render) 트리에서 상태를 전환하는 데 필요한 최소한의 변경을 결정하기 위해 이전 기술(description)과 비교합니다.
Hello world
최소한의 플러터 앱(Flutter app)은 runApp() 함수를 호출하는 간단한 위젯입니다.
화면 중앙에 Hello, world!를 출력해주는 예제 코드입니다.
// 머티리얼 디자인을 구현하는 Flutter 위젯을 사용하기 위해 필요합니다.
import 'package:flutter/material.dart';
// 프로그램이 시작되는 위치를 알려줍니다.
void main() { // 전달받은 위젯을 루트(root) 위젯으로 하며 전체 화면을 꽉채우도록 합니다. // 여기에선 "Hello, world" 텍스트가 화면 중앙에 표시됩니다. runApp( // Center - 포함된 자식 위젯을 중앙에 배치하는 위젯입니다. // 자식 위젯이 괄호안에 포함되어 트리구조를 형성하게 됩니다. // 여기에선 Center와 자식 위젯인 Text가 트리를 구성합니다. const Center( // child - 자식 위젯입니다. // Text - 스타일이 지정된 문자열을 출력합니다. child: Text( // 화면에 출력할 문자열입니다. 'Hello, world!', // textDirection - 텍스트의 방향을 지정합니다. // TextDirection.ltr - 텍스트가 왼쪽에서 오른쪽으로 배치되도록 합니다. textDirection: TextDirection.ltr, ), ), ); } |
MaterialApp를 사용한다면 텍스트 방향을 지정할 필요 없이 자동으로 처리되지만 여기에선 MaterialApp를 사용하고 있지 않기때문에 텍스트 방향을 지정해줘야 합니다. 코드에서 TextDirection.ltr를 사용하여 텍스트가 왼쪽에서 오른쪽으로 배치되도록 하고 있습니다.
위젯이 사용자 입력에 따라 반응하도록 하려면 상태(State)를 관리하는 StatefulWidget를 상속받아 위젯을 생성하고, 위젯이 보여진후 사용자 입력에 따라 반응할 필요가 없다면 상태를 관리하지 않는 StatelessWidget를 상속받아 위젯을 생성합니다. 생성된 이 두 위젯의 주요 작업은 화면에 보여질 자식 위젯으로 구성된 유저 인터페이스를 build() 함수내에서 구현하는 것입니다.
부모 클래스의 생성자(Super constructors)
참고 - https://invertase.io/blog/please-welcome-flutter-3
부모 클래스에 생성자 인수를 전달하기 위해 사용하던 문법이 변경된 것을 설명합니다.
빨간색으로 표시한 부분을 비교해보세요.
예전에 사용하던 문법입니다.
class Animal { Animal(this.name); final String name; } // Old way: before Dart 2.17 class Dog extends Animal { Dog(String name, this.age) : super(name); final int age; } |
이제는 다음처럼 사용합니다.
class Animal { Animal(this.name); final String name; } // New way: Dart 2.17 and above class Dog extends Animal { Employee(super.name, this.age); final int age; } |
Key
참고
https://daldalhanstory.tistory.com/239
MyHomepage({super.key, this.title});
MyHomepage 위젯의 생성자입니다.
{super.key, this.title}의 중괄호는 Dart에서 함수를 정의할 때 선택적 파라미터를 선언하기 위한 구문입니다.
선택적 파라미터는 해당 값을 입력해도 되고 안해도 되는 경우입니다.
파라미터를 반드시 입력받아야 하는 경우에는 다음처럼 required를 해당 파라미터 앞에 추가해줘야 합니다.
MyHomepage({super.key, required this.title});
기본 위젯
Flutter에서 일반적으로 사용되는 위젯은 다음과 같습니다.
Text
스타일이 지정된 텍스트를 화면에 출력합니다.
Row, Column
Row(가로, 행) 방향 또는 Column(세로, 열) 방향으로 위젯을 배치하는 레이아웃입니다.
Stack
선형 방향(행 또는 열 방향) 대신 스택 위젯을 사용하면 위젯을 화면에 그려지는 순서에 따라 배치할 수 있습니다. 먼저 그려진 것이 다음에 그려지는 것에 의해 가려질 수 있습니다.
Container
컨테이너 위젯을 사용하면 직사각형 시각적 요소를 만들 수 있습니다. 컨테이너는 배경, 테두리, 그림자와 같은 BoxDecoration으로 꾸밀 수 있습니다. 또한 컨테이너 크기에 여백, 패딩 등이 적용될 수 있습니다.
화면 상단에 타이틀과 이미지 버튼으로 조합된 Appbar를 보여주는 예제 코드입니다. Appbar는 아래 스크린샷에서 상단에 보이는 파란색 부분입니다. 양쪽 끝에 아이콘이 표시되어 있는 이미지 버튼 2개가 보이는 영역외에 나머지 영역에 타이틀인 Example Title를 출력줍니다. Appbar 외의 영역은 Hello, world!를 출력하는 위젯이 모두 차지하게 됩니다.
// 머티리얼 디자인을 구현하는 Flutter 위젯을 사용하기 위해 필요합니다. import 'package:flutter/material.dart'; // 화면 상단에 보이는 appbar 위젯을 정의한 클래스입니다. // StatelessWidget 위젯을 상속받는 클래스를 선언합니다. // 한번 화면을 출력한 후, 사용자의 입력등에 의해서 변경이 필요없기 때문에 StatelessWidget 위젯을 사용합니다. class MyAppBar extends StatelessWidget { const MyAppBar({required this.title, super.key}); // title - 출력할 문자열입니다. // final을 사용했기 때문에 값이 한번 설정되면 변경할 수 없습니다. final Widget title; @override // build - 화면에 보여질 위젯을 리턴합니다. // MyAppBar 위젯은 좌우 내부 패딩이 8픽셀이고 높이가 56 픽셀인 컨테이너를 만듭니다. // 컨테이너 내부에서 MyAppBar는 Row 레이아웃을 사용하여 자식 위젯들을 수평방향으로 배치합니다. // // StatelessWidget을 상속받아 위젯을 만든 경우 한번 호출됩니다. // 이후 화면이 변경되지 않기 때문입니다. StatefulWidget을 사용한다면 여러번 호출 될 수 있습니다. // 여러번 화면이 변경될 수 있기 때문입니다. Widget build(BuildContext context) { // Container - 일반적인 페인팅, 위치 지정 및 크기 조정 위젯을 결합한 편리한 위젯입니다. // 자식위젯으로 Row가 포함되어 있습니다. return Container( // height - 높이를 지정합니다. 단위 Logical pixel(논리적 픽셀) height: 56.0, // padding - 자식 위젯의 경계와 부모 위젯의 경계 사이의 여백 크기입니다. // EdgeInsets - left, top, right, bottom 매개변수를 기반으로 위젯의 외부 여백 또는 내부 여백을 지정합니다. // EdgeInsets.symmetric - 대칭적으로 수직 또는 수평 여백을 지정합니다. // horizontal: 8.0 - 왼쪽과 오른쪽의 여백을 똑같이 8픽셀로 설정합니다. padding: const EdgeInsets.symmetric(horizontal: 8.0), // decoration - 자식 위젯 뒤에 보일 배경색을 지정합니다. // BoxDecoration(color: ) - 박스 내부를 채울 배경색을 지정합니다. // Colors - 머티리얼 디자인 색상 팔레트를 나타내는 색상 상수입니다. // 값으로 50과 100~900 사이의 값을 100 간격으로 지정할 수 있습니다. 다른값을 사용하면 안됩니다. // 색상 팔레트 참고 https://api.flutter.dev/flutter/material/Colors-class.html decoration: BoxDecoration(color: Colors.blue[500]), // child - 자식 위젯입니다. // Row은 수평 방향의 선형 레이아웃입니다. child: Row( // children : 하나 이상의 자식 위젯을 리스트로 추가합니다. // 자식 위젯으로 IconButton 2개와 Expanded가 포함되어 있고, // Expanded에는 타이틀을 보여줄 title 위젯이 포함되어 있습니다. children: [ // IconButton - 아이콘이 그려진 버튼입니다. const IconButton( // icon - 버튼 내부에 표시할 아이콘입니다. // Icon - 아이콘을 보여주는 위젯입니다. // Icons.menu - menu 이름의 아이콘 icon: Icon(Icons.menu), // tooltip - 버튼을 눌렀을 때 발생할 동작을 설명하는 텍스트로 사용자가 버튼을 길게 누를때 표시됩니다 // 출력되는 문자열입니다. tooltip: 'Navigation menu', // onPressed - 버튼을 활성화할 때 호출되는 콜백입니다. // null로 설정하면 버튼이 비활성화됩니다. onPressed: null, ), // Expanded - 지정한 위젯이 남은 공간을 다 사용하도록 해줍니다. // 여기에선 IconButton를 보여주는 공간외에 나머지를 title 위젯이 차지하도록 해줍니다. Expanded( // child - 자식 위젯입니다. // title - Appbar에 저장될 텍스트를 포함하고 있는 위젯입니다. child: title, ), // IconButton - 아이콘이 그려진 버튼입니다. const IconButton( // icon - 버튼 내부에 표시할 아이콘입니다. // Icon - 아이콘을 보여주는 위젯입니다. // Icons.search - search 이름의 아이콘 icon: Icon(Icons.search), // tooltip - 버튼을 눌렀을 때 발생할 동작을 설명하는 텍스트로 사용자가 버튼을 길게 누를때 표시됩니다. tooltip: 'Search', // onPressed - 버튼을 활성화할 때 호출되는 콜백입니다. // null로 설정하면 버튼이 비활성화됩니다. onPressed: null, ), ], ), // Row ); } } // 화면 전체를 구성하는 위젯을 정의한 클래스입니다. // StatelessWidget 위젯을 상속받는 클래스를 선언하여 위젯을 생성합니다. class MyScaffold extends StatelessWidget { const MyScaffold({super.key}); @override // build - 화면에 보여질 위젯을 리턴합니다. StatelessWidget을 상속받아 위젯을 생성한 경우 한번 호출됩니다. // MyScaffold 위젯은 자식 위젯들을 Column 레이아웃을 사용하여 수직방향으로 배치합니다. // 화면 상단에 MyAppBar를 배치하고 앱바에 제목으로 사용할 Text 위젯을 전달합니다. // Expanded를 사용하여 중앙정렬된 메시지로 나머지 공간을 채웁니다. Widget build(BuildContext context) { // Material - 머티리얼 디자인 가인드라인에 따라 UI를 구성할 수 있도록 구현된 위젯입니다. return Material( // child - 자식 위젯입니다. // Column - 수직방향 선형 레이아웃입니다. 자식 위젯을 수직 방향으로 배치하는 위젯입니다. child: Column( // children - 하나 이상의 자식 위젯을 리스트로 추가합니다. children: [ // appBar입니다. MyAppBar( // title - 출력할 문자열입니다. // Text - 스타일이 지정된 문자열을 출력합니다. title: Text( // title에 이 문자열이 출력됩니다. 'Example title', // 텍스트에 사용할 스타일입니다. style: Theme.of(context) .primaryTextTheme //기본 색상과 대조되는 텍스트 테마 .headline6,//앱 바와 대화 상자의 기본 텍스트에 사용 ), ), // Expanded - 지정한 위젯이 남은 공간을 다 사용하도록 해줍니다. // 여기에선 AppBar를 보여주는 공간외에 나머지를 Text위젯이 차지하도록 해줍니다. const Expanded( // child - 자식 위젯입니다. // Center - 포함된 자식 위젯을 중앙에 배치합니다. child: Center( // child - 자식 위젯입니다. // Text - 스타일이 지정된 문자열을 출력합니다. child: Text('Hello, world!'), ), ), ], ), ); } } // 프로그램이 시작되는 위치를 알려줍니다. void main() { // 전달받은 위젯을 루트 위젯으로 하며 전체 화면을 꽉채우도록 합니다. runApp( // MaterialApp - 머티리얼 디자인 애플리케이션에 일반적으로 필요한 여러 위젯을 래핑하는 편리한 위젯입니다. const MaterialApp( // 장치에서 사용자의 앱을 식별하는 데 사용하는 한 줄 설명입니다. // Android의 경우 테스크 매니저에서 보입니다. // iOS에서는 사용되지 않습니다. title: 'My app', // home - 앱의 기본 경로 위젯(route) // SafeArea - 충분한 패딩을 주어서 디바이스에 따라 위젯이 가려지거나 잘리는 현상을 방지합니다. home: SafeArea( // child - 자식 위젯입니다. // MyScaffold 클래스로 정의된 위젯을 자식 위젯으로 사용합니다. child: MyScaffold(), ), ), ); } |
매터리얼 컴포넌트 사용하기
플러터는 매터리얼 디자인을 따르는 앱을 만드는데 도움이 되는 여러 위젯을 제공합니다. 매터리얼 앱은 "routes"라고도 하는 문자열로 위젯을 식별합니다. 위젯 스택을 관리하는 Navigator를 포함하여 앱 루트에 여러 유용한 위젯을 빌드하는 MaterialApp 위젯으로 시작합니다. Navigator를 사용하면 애플리케이션의 화면 간에 원활하게 전환할 수 있습니다.
// 머티리얼 디자인을 구현하는 Flutter 위젯을 사용하려면 필요합니다. import 'package:flutter/material.dart'; // 프로그램이 시작되는 위치를 알려줍니다. void main() { // 전달받은 위젯을 루트 위젯으로 하며 전체 화면을 꽉채우도록 합니다. runApp( // MaterialApp - 머티리얼 디자인 애플리케이션에 일반적으로 필요한 여러 위젯을 래핑하는 편리한 위젯입니다. const MaterialApp( // 장치에서 사용자의 앱을 식별하는 데 사용하는 한 줄 설명입니다. // Android의 경우 테스크 매니저에서 보입니다. // iOS에서는 사용되지 않습니다. title: 'Flutter Tutorial', // home - 앱의 기본 경로 위젯(route) home: TutorialHome(), ), ); } // 화면 전체를 구성을 정의한 위젯입니다. // StatelessWidget 위젯을 상속받는 클래스를 선언하여 위젯을 정의합니다. class TutorialHome extends StatelessWidget { const TutorialHome({super.key}); @override // build - 화면에 보여질 위젯을 리턴합니다. StatelessWidget을 상속받아 위젯을 생성한 경우 한번 호출됩니다. // 앞에선 MyAppBar, MyScaffold를 사용자 정의해서 사용했는데 여기에선 미리 정의된 AppBar, Scaffold를 사용합니다. Widget build(BuildContext context) { // Scaffold - 머티리얼 디자인의 시각적 레이아웃 구조를 구현합니다. return Scaffold( // appbar 화면 상단에 보여지는 app bar로 화면 상단에 보여지며 툴바와 다른 요소로 구성됩니다. // AppBar - 매터리얼 디자인의 app bar입니다. appBar: AppBar( // leading - toolbar 왼쪽에 보여지게 될 위젯을 의미합니다. // IconButton - 아이콘이 그려진 버튼입니다. leading: const IconButton( // icon - 버튼 내부에 표시할 아이콘입니다. // Icon - 아이콘을 보여주는 위젯입니다. // Icons.menu - menu 이름의 아이콘 icon: Icon(Icons.menu), // tooltip - 버튼을 눌렀을 때 발생할 동작을 설명하는 텍스트로 사용자가 버튼을 길게 누를때 표시됩니다. // 'Navigation menu' - 화면에 표시될 문자열입니다. tooltip: 'Navigation menu', // onPressed - 버튼을 활성화할 때 호출되는 콜백입니다. // null로 설정하면 버튼이 비활성화됩니다. onPressed: null, ), // title - app bar에 출력되는 문자열입니다. // Text - 스타일이 지정된 문자열을 출력합니다. title: const Text('Example title'), // title 다음 줄에 배치되는 위젯 리스트입니다. 여기에선 title의 오른쪽에 배치됩니다. actions: const [ // IconButton - 아이콘이 그려진 버튼입니다. IconButton( // icon - 버튼 내부에 표시할 아이콘입니다. // Icon - 아이콘을 보여주는 위젯입니다. // Icons.search - search 이름의 아이콘 icon: Icon(Icons.search), // tooltip - 버튼을 눌렀을 때 발생할 동작을 설명하는 텍스트로 사용자가 버튼을 길게 누를때 표시됩니다. // 'Search' - 화면에 출력될 문자열입니다. tooltip: 'Search', // onPressed - 버튼을 활성화할 때 호출되는 콜백입니다. // null로 설정하면 버튼이 비활성화됩니다. onPressed: null, ), ], ), // body - appbar 아래에 배치되며 화면에 보여줄 주요 내용입니다. // Center - 포함된 자식 위젯을 중앙에 배치합니다. body: const Center( // child - 자식 위젯입니다. // Text - 스타일이 지정된 문자열을 출력합니다. child: Text('Hello, world!'), ), // floatingActionButton - 화면 오른쪽 하단에 body가 보여지는 영역 위에 떠 있는 버튼으로 표시됩니다. // FloatingActionButton - 원형 아이콘 버튼으로 주요 기능을 위한 용도로 사용해야 합니다. floatingActionButton: const FloatingActionButton( // tooltip - 버튼을 눌렀을 때 발생할 동작을 설명하는 텍스트입니다. 버튼을 길게 누르면 보입니다. tooltip: 'Add', // child - 자식 위젯입니다. // Icon - 아이콘을 보여주는 위젯입니다. // Icons.add - add 이름의 매터리얼 아이콘 child: Icon(Icons.add), // onPressed - 버튼을 활성화할 때 호출되는 콜백입니다. // null로 설정하면 버튼이 비활성화됩니다. onPressed: null, ), ); } } |
제스처 처리
대부분의 응용 프로그램에는 시스템과의 유저 인터페이스가 포함되어 있습니다. 대화형 애플리케이션을 구축하는 첫 번째 단계는 입력 제스처를 감지하는 것입니다. 간단한 버튼을 만들어 작동하는 방법을 확인해봅니다.
// 머티리얼 디자인을 구현하는 Flutter 위젯을 사용하려면 필요합니다. import 'package:flutter/material.dart'; // 버튼을 구성을 정의한 위젯입니다. // StatelessWidget 위젯을 상속받는 클래스를 선언하여 위젯을 정의합니다. class MyButton extends StatelessWidget { const MyButton({super.key}); @override // build - 화면에 보여질 위젯을 리턴합니다. StatelessWidget을 상속 받아 위젯을 생성하는 경우 한번 호출됩니다. Widget build(BuildContext context) { // GestureDetector - 제스처를 검출하는 위젯입니다. return GestureDetector( // 버튼이 터치되었음을 감지할때 호출됩니다. onTap: () { // 콘솔에 메시지를 출력합니다. print('MyButton was tapped!'); }, // child - 자식 위젯입니다. // Container - 일반적인 페인팅, 위치 지정 및 크기 조정 위젯을 결합한 편리한 위젯입니다. // 좌우 여백 8픽셀이고 높이가 50 픽셀인 컨테이너를 생성합니다. // 내부를 초록색으로 채우고 중앙에 'Engage'를 출력하도록 합니다. child: Container( height: 50.0, // padding - 자식 위젯의 경계와 부모 위젯의 경계 사이의 여백 크기입니다. // bottom, top, left, right 4가지 방향으로 여백을 8픽셀 추가합니다. padding: const EdgeInsets.all(8.0), // margin - 부모 위젯과 외부와의 여백 크기입니다. // 좌우로 8픽셀 여백을 추가합니다. margin: const EdgeInsets.symmetric(horizontal: 8.0), // decoration - 자식 위젯 뒤에 보일 배경색을 지정합니다. // BoxDecoration(color: ) - 박스 내부를 채울 배경색을 지정합니다. // BoxDecoration(borderRadius:) - null이 아니면 이 상자의 모서리가 이 BorderRadius에 의해 둥글게 처리됩니다. // BorderRadius.circular - 사각형의 코너를 둥글게 만들어줍니다. // Colors - 머티리얼 디자인 색상 팔레트를 나타내는 색상 상수입니다. // 값으로 50과 100~900 사이의 값을 100 간격으로 지정할 수 있습니다. 다른값을 사용하면 안됩니다. // 색상 팔레트 참고 https://api.flutter.dev/flutter/material/Colors-class.html decoration: BoxDecoration( borderRadius: BorderRadius.circular(5.0), color: Colors.lightGreen[500], ), // child - 자식 위젯입니다. // Center - 포함된 자식 위젯을 중앙에 배치합니다. child: const Center( // child - 자식 위젯입니다. // Text - 스타일이 지정된 문자열을 출력합니다. child: Text('Engage'), ), ), ); } } // 프로그램이 시작되는 위치를 알려줍니다. void main() { // 전달받은 위젯을 루트 위젯으로 하며 전체 화면을 꽉채우도록 합니다. runApp( // MaterialApp - 머티리얼 디자인 애플리케이션에 일반적으로 필요한 여러 위젯을 래핑하는 편리한 위젯입니다. const MaterialApp( // home - 앱의 기본 경로 위젯(route) // Scaffold - 머티리얼 디자인의 시각적 레이아웃 구조를 구현합니다. home: Scaffold( // body - scaffold의 기본 콘텐츠입니다. // Center - 포함된 자식 위젯을 중앙에 배치합니다. body: Center( // child - 자식 위젯입니다. child: MyButton(), ), ), ), ); } |
GestureDetector 위젯에는 시각적 표현이 없지만 대신 사용자가 만든 제스처를 감지합니다. 사용자가 컨테이너를 탭하면 GestureDetector는 onTap() 콜백을 호출하며 이 경우 콘솔에 메시지를 출력합니다. GestureDetector를 사용하여 탭, 드래그 및 스케일을 포함한 다양한 입력 제스처를 감지할 수 있습니다.
입력에 대한 응답으로 위젯 변경
지금까지는 Stateless 위젯만 사용했습니다. Stateless 위젯은 상위 위젯에서 아규먼트를 수신하여 final 멤버 변수에 저장합니다. 위젯에 build()를 요청하면 멤버 변수에 저장된 값을 사용하여 생성하는 위젯을 만드는데 사용합니다. final 멤버 변수에 저장된 값은 변경할 수 없기 때문에 사용자의 입력등에 따라 위젯에 변화를 줄 수 없습니다.
사용자 입력에 따라 위젯이 변하도록 하려면 상태를 전달받아야 합니다. Flutter는 이를 위해 StatefulWidget을 사용합니다. StatefulWidget은 상태를 저장하기 위해 State 객체를 생성하여 사용합니다.
// 머티리얼 디자인을 구현하는 Flutter 위젯을 사용하려면 필요합니다. import 'package:flutter/material.dart'; // StatefulWidget 위젯을 상속받는 클래스를 선언합니다. class Counter extends StatefulWidget { const Counter({super.key}); // StatefulWidget 위젯 객체가 생성되면 자신의 상태를 관리할 State 위젯 객체를 생성하여 연결합니다. // 이후 동일한 StatefulWidget 위젯을 새로 생성하여 사용하는 경우엔 기존에 생성했던 State 위젯 객체를 재사용합니다. @override _CounterState createState() => _CounterState(); } // State 위젯을 상속받은 클래스를 선언하여 위젯을 정의합니다. // Counter - StatefulWidget 위젯입니다. class _CounterState extends State<Counter> { int _counter = 0; void _increment() { // setState내에서 값을 변경하여 플러터 프레임워크에 상태에 변경 사항이 있음을 알립니다. // 이로 인해 화면에 업데이트된 값이 반영될 수 있도록 build 메서드가 다시 실행됩니다. // setState()를 호출하지 않고 _counter를 변경하면 build 메서드가 호출되지 않으므로 화면에 변화가 없습니다. setState(() { _counter++; }); } @override // build - 화면에 보여질 위젯을 리턴합니다. StatefulWidget을 상속받아 위젯을 생성한 경우 여러번 호출이 가능합니다. // 이 메서드는 위의 _increment 메서드에서 수행한 것처럼 setState가 호출될 때마다 다시 실행됩니다. Widget build(BuildContext context) { // Row은 수평 방향의 선형 레이아웃입니다. return Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ ElevatedButton( // onPressed - 버튼을 활성화할 때 호출되는 콜백입니다. // null로 설정하면 버튼이 비활성화됩니다. onPressed: _increment, // child - 자식 위젯입니다. // Text - 스타일이 지정된 문자열을 출력합니다. child: const Text('Increment'), ), // SizedBox - 지정된 크기의 직사각형 위젯입니다. 빈공간을 채우기 위해 사용합니다. const SizedBox(width: 16), // Text - 스타일이 지정된 문자열을 출력합니다. Text('Count: $_counter'), ], ); } } // 프로그램이 시작되는 위치를 알려줍니다. void main() { // 전달받은 위젯을 루트 위젯으로 하며 전체 화면을 꽉채우도록 합니다. runApp( // MaterialApp - 머티리얼 디자인 애플리케이션에 일반적으로 필요한 여러 위젯을 래핑하는 편리한 위젯입니다. const MaterialApp( // home - 앱의 기본 경로 위젯(route) // Scaffold - 머티리얼 디자인의 시각적 레이아웃 구조를 구현합니다. home: Scaffold( // body - scaffold의 기본 콘텐츠입니다. // Center - 포함된 자식 위젯을 중앙에 배치합니다. body: Center( // child - 자식 위젯입니다. child: Counter(), ), ), ), ); } |
StatefulWidget과 State가 별도의 객체인 이유가 궁금할 것입니다. Flutter에서 이 두 가지 유형의 객체는 수명 주기가 다릅니다. StatefulWidget 위젯은 현재 상태에서 애플리케이션의 화면을 구성하는 데 사용되는 임시 개체입니다. 반면에 State 객체는 build() 호출시에도 영구적이므로 위젯 상태 정보를 기억할 수 있습니다.
위의 예는 사용자가 버튼을 클릭하면 build() 메서드에서 Count 옆에 숫자를 증가시키기 위해 화면을 업데이트합니다.
counter 값을 화면에 표시하는 CounterDisplay와 counter 값을 변경하는 CounterIncrementor로 하나의 위젯을 두개로 분리한 예제입니다.
실행 결과는 이전과 동일합니다.
// 머티리얼 디자인을 구현하는 Flutter 위젯을 사용하려면 필요합니다. import 'package:flutter/material.dart'; // StatelessWidget 위젯을 상속받는 클래스를 선언합니다. class CounterDisplay extends StatelessWidget { const CounterDisplay({required this.count, super.key}); final int count; @override // build - 화면에 보여질 위젯을 리턴합니다. StatelessWidget의 경우 한번 호출됩니다. Widget build(BuildContext context) { // Text - 스타일이 지정된 문자열을 출력합니다. return Text('Count: $count'); } } // StatelessWidget 위젯을 상속받는 클래스를 선언합니다. class CounterIncrementor extends StatelessWidget { const CounterIncrementor({required this.onPressed, super.key}); final VoidCallback onPressed; @override // build - 화면에 보여질 위젯을 리턴합니다. StatelessWidget의 경우 한번 호출됩니다. Widget build(BuildContext context) { return ElevatedButton( // onPressed - 버튼을 활성화할 때 호출되는 콜백입니다. // null로 설정하면 버튼이 비활성화됩니다. onPressed: onPressed, // child - 자식 위젯입니다. // Text - 스타일이 지정된 문자열을 출력합니다. child: const Text('Increment'), ); } } // StatefulWidget 위젯을 상속받는 클래스를 선언하여 위젯을 정의합니다. class Counter extends StatefulWidget { const Counter({super.key}); // StatefulWidget 위젯 객체가 생성되면 자신의 상태를 관리할 State 위젯 객체를 생성하여 연결합니다. // 이후 동일한 StatefulWidget 위젯을 새로 생성하여 사용하는 경우엔 기존에 생성했던 State 위젯 객체를 재사용합니다. @override _CounterState createState() => _CounterState(); } // State 위젯을 상속받은 클래스를 선언하여 위젯을 정의합니다. // Counter - 위젯의 상태를 저장할 StatefulWidget 위젯입니다. class _CounterState extends State<Counter> { int _counter = 0; void _increment() { // setState내에서 값을 변경하여 플러터 프레임워크에 상태에 변경 사항이 있음을 알립니다. // 이로 인해 화면에 업데이트된 값이 반영될 수 있도록 build 메서드가 다시 실행됩니다. // setState()를 호출하지 않고 _counter를 변경하면 build 메서드가 호출되지 않으므로 화면에 변화가 없습니다. setState(() { ++_counter; }); } @override // build - 화면에 보여질 위젯을 리턴합니다. StatelessWidget의 경우 한번 호출됩니다. Widget build(BuildContext context) { // Row은 수평 방향의 선형 레이아웃입니다. return Row( // mainAxisAlignment - 자식 위젯이 메인 축을 따라 어떻게 배치되는지 결정합니다. // MainAxisAlignment.center - 메인 축의 중앙에 자식 위젯을 배치합니다. mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ // 앞에서 StatelessWidget을 상속받아 정의한 위젯 클래스입니다. CounterIncrementor( // onPressed - 버튼을 활성화할 때 호출되는 콜백입니다. // null로 설정하면 버튼이 비활성화됩니다. onPressed: _increment), // SizedBox - 지정된 크기의 직사각형 위젯입니다. 빈공간을 채우기 위해 사용합니다. const SizedBox(width: 16), // 앞에서 StatelessWidget을 상속받아 정의한 위젯 클래스입니다. CounterDisplay(count: _counter), ], ); } } // 프로그램이 시작되는 위치를 알려줍니다. void main() { // 전달받은 위젯을 루트 위젯으로 하며 전체 화면을 꽉채우도록 합니다. runApp( // MaterialApp - 머티리얼 디자인 애플리케이션에 일반적으로 필요한 여러 위젯을 래핑하는 편리한 위젯입니다. const MaterialApp( // home - 앱의 기본 경로 위젯(route) // Scaffold - 머티리얼 디자인의 시각적 레이아웃 구조를 구현합니다. home: Scaffold( // body - scaffold의 기본 콘텐츠입니다. // Center - 포함된 자식 위젯을 중앙에 배치합니다. body: Center( // child - 자식 위젯입니다. // Counter 위젯을 자식으로 사용합니다. child: Counter(), ), ), ), ); } |
모든 것을 하나로 모으기
가상의 쇼핑 애플리케이션을 만들어 봅니다. 판매 제품을 표시하고 구매를 선택시 장바구니에 들어가도록합니다.
// 머티리얼 디자인을 구현하는 Flutter 위젯을 사용하려면 필요합니다. import 'package:flutter/material.dart'; // 제품을 나타내는 클래스로 속성으로 제품 이름을 갖는 name이 있습니다. class Product { const Product({required this.name}); final String name; } // 함수를 호출시 CartChangedCallback 타입의 변수 이름을 Function를 호출하는 함수 이름으로 사용할 수 있습니다. typedef CartChangedCallback = Function(Product product, bool inCart); // StatelessWidget 위젯을 상속받는 클래스를 선언합니다. class ShoppingListItem extends StatelessWidget { // ShoppingListItem 객체가 생성되면서 product, inCart, CartChangedCallBack을 멤버 변수에 할당받아 // ShoppingListItem 객체가 소멸될때까지 값변경없이 유지합니다. ShoppingListItem({ required this.product, required this.inCart, required this.onCartChanged, }) : super(key: ObjectKey(product)); final Product product; final bool inCart; // 앞에서 typedef로 지정한 함수 호출시 onCartChanged 이름을 사용할 수 있습니다. // ShoppingListItem 객체 생성시 onCartChanged를 위해 지정한 _handleCartChanged가 호출되도록 하기 위해 사용합니다. final CartChangedCallback onCartChanged; // inCart가 True이면 회색, 아니면 강조색을 리턴합니다. 여기에선 강조색이 파란색입니다. Color _getColor(BuildContext context) { return inCart ? Colors.black54 : Theme.of(context).primaryColor; } // 카드에 들어있는 경우 즉 inCart가 True인 경우 글씨를 희색으로 바꾸로 취소선을 글씨 타입에 추가합니다. // 카드에 포함되어 있지 않은 경우 _getColor에서 지정해놓은 파란색 글씨로 보이게 됩니다. TextStyle? _getTextStyle(BuildContext context) { if (!inCart) return null; // 텍스트의 스타일과 색을 지정합니다. return const TextStyle( // black, black87, black54, black45, black38, black26, black12 // 모두 검은색이나 불투명도가 다릅니다. 오른쪽으로 갈수록 불투명도가 낮아집니다. // 참고 https://api.flutter.dev/flutter/material/Colors/black-constant.html color: Colors.black54, // 글씨에 취소선을 긋습니다. decoration: TextDecoration.lineThrough, ); } @override // build - 화면에 보여질 위젯을 리턴합니다. StatelessWidget의 경우 한번 호출됩니다. Widget build(BuildContext context) { // ListTile - 텍스트와 아이콘을 포함하는 고정 높이의 단일 행으로 1~3줄의 텍스트가 포함됩니다. return ListTile( // onTap - 사용자가 List Tile을 탭하면 onCartChanged 함수가 호출됩니다. onTap: () { onCartChanged(product, inCart); }, // leading - title 전에 출력되는 위젯입니다. // 원을 그려줍니다. leading: CircleAvatar( // backgroundColor - 원을 채울 색입니다. // inCart값에 따라 _getColor함수에서 지정되는 색을 원을 채울 색으로 사용합니다. backgroundColor: _getColor(context), // child - 자식 위젯입니다. // Text - 스타일이 지정된 문자열을 출력합니다. // product에 저장된 이름의 첫번째 글자를 원안에 출력합니다. child: Text(product.name[0]), ), // title - List Title의 기본 콘텐츠입니다. // Text - 스타일이 지정된 문자열을 출력합니다. // 제품의 전체 이름을 출력합니다. title: Text( product.name, // 텍스트에 사용할 스타일입니다. // inCart값에 따라 _getTextStyle함수에서 지정되는 스타일을 글자를 위한 스타일로 사용합니다. style: _getTextStyle(context), ), ); } } // StatefulWidget 위젯을 상속받는 클래스를 선언하여 위젯을 정의합니다. class ShoppingList extends StatefulWidget { // 리스트에 보여줄 제품 리스트를 products로 전달 받습니다. const ShoppingList({required this.products, super.key}); // State 위젯 객체의 build 메소드에서 사용하는 값을 가지고 있습니다. // 리스트에 출력할 제품이름 리스트입니다. final List<Product> products; // StatefulWidget 위젯 객체가 생성되면 자신의 상태를 관리할 State 위젯 객체를 생성하여 연결합니다. // 이후 동일한 StatefulWidget 위젯을 새로 생성하여 사용하는 경우엔 기존에 생성했던 State 위젯 객체를 재사용합니다. @override _ShoppingListState createState() => _ShoppingListState(); } // State 위젯을 상속받은 클래스를 선언하여 위젯을 정의합니다. // ShoppingList - 위젯의 상태를 저장할 StatefulWidget 위젯입니다. class _ShoppingListState extends State<ShoppingList> { // 쇼핑 카드는 비워진 상태로 초기화됩니다. ShoppingList 위젯에서 보여질 데이터를 State객체에서 저장하고 있습니다. final _shoppingCart = <Product>{}; void _handleCartChanged(Product product, bool inCart) { // 사용자가 장바구니에 있는 것을 변경하면 setState 호출하여 _shoppingCart를 변경합니다. // 그런 다음 프레임워크는 화면에 업데이트된 값이 반영될 수 있도록 build 메서드를 호출합니다. setState(() { if (!inCart) { _shoppingCart.add(product); } else { _shoppingCart.remove(product); } }); } @override // build - 화면에 보여질 위젯을 리턴합니다. Widget build(BuildContext context) { // Scaffold - 머티리얼 디자인의 시각적 레이아웃 구조를 구현합니다. return Scaffold( // appbar 화면 상단에 보여지는 app bar로 화면 상단에 보여지며 툴바와 다른 요소로 구성됩니다. // AppBar - 매터리얼 디자인의 app bar입니다. appBar: AppBar( // title - app bar에 출력되는 문자열입니다. // Text - 스타일이 지정된 문자열을 출력합니다. title: const Text('Shopping List'), ), // body - scaffold의 기본 콘텐츠입니다. // ListView - 선형으로 정렬된 스크롤 가능한 위젯 리스트입니다. body: ListView( // padding - 자식 위젯의 경계와 부모 위젯의 경계 사이의 여백 크기입니다. padding: const EdgeInsets.symmetric(vertical: 8.0), // children - 스크롤 방향으로 여기에 지정한 위젯을 일렬로 리스트뷰에 표시합니다. // 리스트 products에 있는 제품 정보 product를 가지고 ShoppingListItem 객체를 저장하는 리스트로 바꿉니다. children: widget.products.map((product) { return ShoppingListItem( // 리스트에서 제품 정보를 저장합니다. product: product, // 리스트에서 제품이 카트에 포함여부를 bool로 저장합니다. inCart: _shoppingCart.contains(product), // 쇼핑 카드의 내용이 변경되면 onCartChanged를 위해 지정한 _handleCartChanged 함수가 호출되도록 지정해줍니다. onCartChanged: _handleCartChanged, ); }).toList(), ), ); } } // 프로그램이 시작되는 위치를 알려줍니다. void main() { // 전달받은 위젯을 루트 위젯으로 하며 전체 화면을 꽉채우도록 합니다. runApp( // MaterialApp - 머티리얼 디자인 애플리케이션에 일반적으로 필요한 여러 위젯을 래핑하는 편리한 위젯입니다. const MaterialApp( // 장치에서 사용자의 앱을 식별하는 데 사용하는 한 줄 설명입니다. // Android의 경우 테스크 매니저에서 보입니다. // iOS에서는 사용되지 않습니다. title: 'Shopping App', // home - 앱의 기본 경로 위젯(route) // ShoppingList 위젯이 화면에 보여집니다. home: ShoppingList( // 화면에 보여질 제품 리스트입니다. products: [ Product(name: 'Eggs'), Product(name: 'Flour'), Product(name: 'Chocolate chips'), ], ), )); } |
실행화면입니다.제품을 터치하면 장바구니에 해당 제품이 추가되면서 제품이름 앞의 원 색상이 회색으로 변하게 됩니다. 다시 해당 제품을 터치하면 장바구니에서 삭제되며 제품이름 앞의 원 색상이 파란색으로 변합니다.
'Flutter > Flutter 강좌' 카테고리의 다른 글
Flutter 프로젝트에 이미지 파일 추가하여 사용하기 (0) | 2023.10.18 |
---|---|
Flutter 강좌 04 - 레이아웃 (0) | 2023.10.18 |
Flutter 강좌 02 - Flutter에서 StatefulWidget과 StatelessWidget의 차이 정리 (0) | 2023.10.18 |
Flutter 강좌 01 - 첫번째 앱 작성하기 (0) | 2023.10.18 |
시간날때마다 틈틈이 이것저것 해보며 블로그에 글을 남깁니다.
블로그의 문서는 종종 최신 버전으로 업데이트됩니다.
여유 시간이 날때 진행하는 거라 언제 진행될지는 알 수 없습니다.
영화,책, 생각등을 올리는 블로그도 운영하고 있습니다.
https://freewriting2024.tistory.com
제가 쓴 책도 한번 검토해보세요 ^^
그렇게 천천히 걸으면서도 그렇게 빨리 앞으로 나갈 수 있다는 건.
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!