[개념]

Flutter SDK, VScode, Android Studio 설치
1. Flutter 설치
여기서 다운로드 후 zip 압축을 푼다.

C: 폴더에 저장해 둔다.
2. 환경 변수 설정
wimdow 검색 창에 ‘시스템 환경 변수 편집’을 검색 한다.

‘환경 변수’를 클릭 한다.

시스템 변수에서 Path 새로 만들기를 클릭한다.

아까 Flutter SDK 압축을 풀었던 폴더에서 bin 경로를 복사하고, 새로 만들기 한 곳에 붙여 넣기한다.

cmd 창에 flutter —version 쳤을 때 밑과 같이 뜨면 성공!
3. 안드로이드 스튜디오 설치
여기서 다운로드를 진행한다.

이것만 OK 해주기, 나머지는 next를 누르면 된다.

Plugins 들어가서 Flutter를 설치한다.
마지막으로 licenses 바꿔주면 끝!


Apply까지 한 이후에는 cmd에 들어가 flutter doctor --android-licenses 입력 후 y를 누른다.
Android toolchain 카테고리에 체크 표시가 되어있으면 성공!

4. VScode 설치
위 링크에 들어가서 다운을 받는다.
C:\Users\jjw31\AppData\Local\Programs\Microsoft VS Code 나는 여기에 저장했다.
하지만, 왼쪽 위의 view→command palette에 들어가 flutter doctor를 실행해보면, 두 가지가 아직 X표시 인 것을 확인할 수 있다. (아까는 crome도 X였지만, crome설치 후 해결했음)

이는 Visual Studio를 설치함으로써 해결할 수 있다.
F5 or ctrl F5 누르면, 실행이 된다
후에 command palette에서 emulator로 휴대폰 모양 가져올 수도 있음! 성공! (Flutter:select device에서)
프로젝트 생성은 View → Command palette → New project → Application을 순서대로 누르면 된다.
Dart 언어
1. Dart 언어
Dart Pad : https://dartpad.dev/ 들어가면, 온라인에서 dart어 연습할 수 있다.
- OOP : 객체지향 프로그래밍
dart는 객체지향 프로그래밍
객체지향 프로그래밍이란 각각의 클래스가 만든 객체가 행위의 주체가 되는 프로그래밍
클래스 : 객체를 만들기 위한 일종의 템플릿, 내부에 정의된 변수와 함수(메서드)로 구성되어 있음 게임으로 비유하면 캐릭터 직업.
객체 : 클래스로 생성한 하나의 개체, 클래스에서 정의한 변수와 메서드를 사용할 수 있음. 게임으로 비유하면 유저가 생성한 캐릭터.
메서드 : 클래스 내부에서 선언된 함수, 객체가 사용할 수 있음. 게임으로 비유하면 해당 직업의 스킬로 캐릭터가 시전.
상속 : 이미 만들어진 클래스(부모)를 기반으로 몇 가지 기능을 추가해 새로 클래스(자식)를 만드는 것.
오버라이딩 : 클래스를 상속받았을 때 부모 클래스의 메서드를 수정해서 사용하는 것.
EX. 로그인 버튼을 하나의 객체로 만든다고 해보면, 버튼 클래스에는 버튼을 눌렀을 때 어떤 일이 일어나는지 정의된다. 로그인 버튼, 구매 버튼 등 상속을 이용하여 여러 버튼을 구현할 수도 있고, 같은 종류의 버튼이라도 이벤트가 다를 수 있다 (메서드 오버라이딩).
- 비동기 프로그래밍
실행된 작업이 오래 걸릴 경우 다른 작업들을 계속 수행하며, 이와 동시에 다음 단계까지 수행하는 프로그래밍. 작업을 기다릴 필요가 없게 된다.
Dart에서는 Future라는 키워드를 이용해서 비동기 프로그래밍을 정의할 수 있다.
- Null-Safety
이 컨셉은 nullable, non-nullable로 나누어진다.
먼저 nullable은 ?를 이용해서 정의한다. 이 때 변수에 초기 값을 넣지 않더라도 에러는 발생하지 않는다.
(자세한 건 책 참고)
Flutter 기본 개념 알아가기
1. 플러터 설계 컨셉
- 위젯(Widget)
플러터는 하나하나 모두 위젯으로 구성되어 있다. 이러한 위젯들이 모여서 트리 형태를 구성한다. 하나의 큰 위젯안에 다른 위젯이 들어있고, 다른 위젯 안에 또 다른 위젯이 들어있고…
위젯은 클래스의 형태를 갖추고 있다. 위젯을 만드는 클래스를 작성하고, 이 클래스로 객체를 생성하며 화면을 구성한다.
- 반응성(Reactive)
‘상태’는 굉장히 중요하다. 우리는 이를 관리하는 걸 배워야 한다. 특히나 UI에서의 상태는 사람들이 직접 보는 부분이기 때문에 더 중요하다.
bool isAirOn; // True if On, False if off
에어컨의 상태를 다루는 변수 표현
setState() { isAirOn = !isAirOn; }
에어컨의 상태는 버튼 클릭에 따라 변한다. 전원 버튼이 눌리면 상태는 위와 같이 반대로 바뀐다.
- Stream
Stream은 data의 흐름을 의미한다.
유튜브 등의 영상 플랫폼에서 영상을 볼 때, 우리는 영상 전체를 받아오지 않고, 영상을 보면서 조금씩 data를 받아오게 된다.
2. 플러터 프로젝트의 구조
- 기본 생성 프로젝트의 구조
프로젝트 이름으로 생성된 폴더를 root라고 하면, 그 밑에는 android, ios, web이라는 폴더가 있다.
‘.’으로 시작하는 파일들은 대부분 우리가 고려하지 않아도 된다. .gitignore은 Git 커밋에 반영되지 않도록 하는 설정 파일이다. 이것만 알고 있자.
pubspec.yaml 파일과 main.dart 파일만 알아두면 된다.
- Pubspec.yaml
이 파일은 플러터 프로젝트의 메타 정보를 담고 있는 마크 업 파일이다.
메타 정보란 프로젝트의 이름, 설명, 버전 등의 정보를 의미한다.
- 사용할 패키지 가져오기
- 이미지 가져오기
- 폰트 가져오기
- main.dart
우선 lib 폴더는 앞으로 우리가 만드는 모든 dart 파일을 저장 할 공간이다.
프로젝트 내의 소스 코드가 모두 여기에 저장 된다.
main.dart는 lib 폴더 안에 들어가 있고, 프로젝트의 시작이 되는 소스 코드이다.
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( // This is the theme of your application. // // TRY THIS: Try running your application with "flutter run". You'll see // the application has a blue toolbar. Then, without quitting the app, // try changing the seedColor in the colorScheme below to Colors.green // and then invoke "hot reload" (save your changes or press the "hot // reload" button in a Flutter-supported IDE, or press "r" if you used // the command line to start the app). // // Notice that the counter didn't reset back to zero; the application // state is not lost during the reload. To reset the state, use hot // restart instead. // // This works for code too, not just values: Most code changes can be // tested with just a hot reload. colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), // color 바꿀 수 있 useMaterial3: true, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); // This widget is the home page of your application. It is stateful, meaning // that it has a State object (defined below) that contains fields that affect // how it looks. // This class is the configuration for the state. It holds the values (in this // case the title) provided by the parent (in this case the App widget) and // used by the build method of the State. Fields in a Widget subclass are // always marked "final". final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState(() { // This call to setState tells the Flutter framework that something has // changed in this State, which causes it to rerun the build method below // so that the display can reflect the updated values. If we changed // _counter without calling setState(), then the build method would not be // called again, and so nothing would appear to happen. _counter++; }); } @override Widget build(BuildContext context) { // This method is rerun every time setState is called, for instance as done // by the _incrementCounter method above. // // The Flutter framework has been optimized to make rerunning build methods // fast, so that you can just rebuild anything that needs updating rather // than having to individually change instances of widgets. return Scaffold( appBar: AppBar( // TRY THIS: Try changing the color here to a specific color (to // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar // change color while the other colors stay the same. backgroundColor: Theme.of(context).colorScheme.inversePrimary, // Here we take the value from the MyHomePage object that was created by // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), body: Center( // Center is a layout widget. It takes a single child and positions it // in the middle of the parent. child: Column( // Column is also a layout widget. It takes a list of children and // arranges them vertically. By default, it sizes itself to fit its // children horizontally, and tries to be as tall as its parent. // // Column has various properties to control how it sizes itself and // how it positions its children. Here we use mainAxisAlignment to // center the children vertically; the main axis here is the vertical // axis because Columns are vertical (the cross axis would be // horizontal). // // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" // action in the IDE, or press "p" in the console), to see the // wireframe for each widget. mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headlineMedium, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ); } }
- import를 통해서 meterial.dart 라는 라이브러리를 불러온다.
- main 함수를 실행한다. 여기서는 main 함수에서 runApp((MyApp());이라는 명령어를 통해 App을 실행하고 있다.
- 위에서 실행한 MyApp을 정의하는 코드이다. 클래스의 형태이고, extends 구문을 통해 상속을 하고 있다. 상속 받는 대상은 StatelessWidget이고, 이는 간단하게 상태(state)가 없는(less) 위젯(Widget)을 의미한다. 즉 MyApp은 하나의 위젯인 것이다.
- 모든 위젯들은 공통적으로 Widget build(BuildContext context)를 가지고 있다. 이는 부모 위젯, StatefulWidget의 메서드 이므로 @Override가 붙은 것을 확인할 수 있다. BuildContext라는 것은 모든 위젯이 가지고 있는 고유의 아이디 값이다. 위젯이 어디 있는지 나타내는 파라미터라고 이해하면 된다.
- Widget build의 리턴으로 MaterialApp() 위젯이 반환되고 있다. 이는 말 그대로 Material 스타일의 App 위젯이며, 여기에서 비로소 앱이라는 큰 위젯이 시작된다.
- MaterialApp의 home property에는 첫 화면을 나타내는 위젯이 들어가야 한다. (질문 : MyhomePage에서도 바꿀 수 있는 것인가?)
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( // This is the theme of your application. // // TRY THIS: Try running your application with "flutter run". You'll see // the application has a blue toolbar. Then, without quitting the app, // try changing the seedColor in the colorScheme below to Colors.green // and then invoke "hot reload" (save your changes or press the "hot // reload" button in a Flutter-supported IDE, or press "r" if you used // the command line to start the app). // // Notice that the counter didn't reset back to zero; the application // state is not lost during the reload. To reset the state, use hot // restart instead. // // This works for code too, not just values: Most code changes can be // tested with just a hot reload. colorScheme: ColorScheme.fromSeed(seedColor: Color.fromARGB(255, 169, 58, 233)), useMaterial3: true, ), home: MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatelessWidget { // extends --> '상속 받았다' final String title; MyHomePage({required this.title}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( title, ), ), body: Center(child: Text('Hello, Flutter!')), ); } }

위와 같이 MyHomePage 클래스를 바꾸어 작성하면, 홈 화면이 다음과 같이 수정된다.
위 코드는 어떻게 작성 되었는지 알아보자.
- StatelessWidget을 상속받은 MyHomePage 클래스 안에는 String title이라는 프로퍼티가 있다. 이는 상단에 표시될 제목을 의미한다. 여기 앞에 final 키워드가 붙었는데, 이는 제목이 나중에 변경되지 않도록 돕는다. title값은 오직 생성자에서만 설정할 수 있다.
- 생성자는 MyHomePage({});와 같이 간단하게 선언 할 수 있다. 우리는 this.title을 통해 title을 받아오도록 선언한다.
- Scaffold()는 앱의 화면이 기본적으로 갖추고 있는 기능들을 선언해 놓은 위젯이다. 여기서는 최상단의 appbar, 가운데 컨텐츠가 들어가는 body, 하단 네비게이션바를 사용할 수 있다.
- 우리는 이 중 appbar와 body를 사용했다. appbar에서는 Appbar()라는 위젯을 가져왔으며, Appbar() 위젯의 프로퍼티인 title을 Text() 위젯으로 생성했다. Text() 위젯의 프로퍼티인 title에는 앞서 선언한 앱의 title 값을 가져와 사용했다.
- body에는 Center() 위젯을 가져와 넣었다. Center() 위젯엔 Text() 위젯이 들어가고, Text() 위젯의 title 값에는 ‘Hello, Flutter’을 넣었다.
이를 읽어보면 대충 어떤 구조인지 파악 가능!
기본 widget 사용법
1. Container/Padding/Center: 가장 기초가 되는 위젯
- Container 위젯 사용하기
Container는 단어의 뜻 그대로 큰 상자처럼 위젯을 담는 역할을 한다. Container는 위젯을 감싸줄 뿐만 아니라 여러 부가 기능들도 제공해준다.
첫 번째가 여백을 설정할 수 있게 하는 padding 프로퍼티이다.
Body에 한 번 container 위젯을 넣어보도록 하자.
container에는 child가 필요하다. 우리가 아는 text() 위젯을 적용해보자
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Color.fromARGB(255, 169, 58, 233)), useMaterial3: true, ), home: MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatelessWidget { final String title; MyHomePage({required this.title}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( title, ), ), body: Container(child: Text('Hello, Flutter!'))); } }
다음과 같은 화면이 나타나게 된다.

이 때 배치와 관련된 명령을 하지 않았고, padding 프로퍼티도 설정되지 않았기 때문에 죄측 상단에 여백 없이 출력된 것을 확인할 수 있다.
padding 프로퍼티를 적용하면 다음과 같은 화면이 나타나게 된다 (child와 padding 사이에 , 넣어주어야 한다).
padding: EdgeInsets.all(30)
밑과 같이 적어도 된다.
body: Container( child: Text('Hello, Flutter!'), padding: EdgeInsets.all(30)));

padding 프로퍼티에서는 EdgeInsets라는 위젯을 사용한다.
위에서는 EdgeInsets.all(30) 값을 사용했는데, 이는 상하좌우 모든 방향에 30만큼, 똑같은 크기에 여백을 두겠다는 의미이다.
밑의 코드처럼 all이외에도 여백을 설정할 수 있는 방식은 다양하다.
EdgeInsets.fromTRB(left: , top: , right: , bottom:) // 좌 상 우 하 방향으로 여백을 설정한다. EdgeInsects.only(left: ) // 한 방향에 대해서만 여백을 설정한다. EdgeInsects.symmetric(vertical: , horizontal:) // 세로 방향, 가로 방향에 대한 여백을 설정한다.
- 여백만을 위한 Padding 위젯
여백만을 설정하고 싶다면, Padding 위젯을 사용해도 된다.
Padding 위젯의 가장 큰 특징은 프로퍼티로 padding과 child만을 갖고 있다는 점이다.
밑과 같이 적으면 된다. (Padding 위젯의 첫 글자는 대문자, padding 프로퍼티의 첫 글자는 소문자인 것을 명심하자)
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Color.fromARGB(255, 169, 58, 233)), useMaterial3: true, ), home: MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatelessWidget { final String title; MyHomePage({required this.title}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( title, ), ), body: Padding( child: Text('Hello Flutter!'), padding: EdgeInsets.all(30), )); } }
Container 위젯에서는 color 프로퍼티로 상자 배경색을 설정할 수도 있다.
또한 width와 height 프로퍼티로 상자의 너비와 높이도 설정할 수 있다.
밑과 같이 작성하면 된다.
body: Container( child: Text('Hello, Flutter!'), padding: EdgeInsets.all(30), color: Colors.blue, height: 300, width: 300));

- Center 위젯
Container 위젯과 비슷한 위젯으로 Center 위젯이 있다.
이는 말 그대로 가운데에 프로퍼티들을 배치시키는 위젯이다.
2. Image: 이미지 넣기
image 위젯은 이미지를 불러오기 위한 위젯이다.
- 로컬의 이미지를 불러오는 방법
- 인터넷의 이미지를 불러오는 방법
위 두 가지 방식으로 이미지를 불러온다.
먼저 로컬 이미지를 화면에 넣어보자.
- 로컬 이미지 넣기
- 먼저 이미지를 프로젝트에 추가해준다.
프로젝트 루트 디렉토리에 images 폴더를 만들고, 그 안에 이미지를 넣는다. (.png 파일을 추가해주면 자동으로 이미지 파일이 생성되고. 거기에 긁어서 옮기면 이미지가 삽입된다)

- 다음 단계는 pubspec.yaml 파일에 이미지를 등록하는 것이다.
flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: assets: - images/airplane.png
pubspec.yaml 파일에 이미지를 입력하는 부분을 다음과 같이 수정한다. 처음에 할 때는 주석 처리가 되어있으므로 주석을 제거하고 진행해야 한다.
- 마지막으로 이미지 위젯을 사용한다.
body: Container(child: Image.asset('images/airplane.png')));
child 프로퍼티에 image.asset 위젯을 입력해주면 된다.
Hot-Restart를 이용해서 실행해보면 (그냥 저장하면 원래는 바뀌어서 출력되지만, 이 때는 오류가 생기므로 Hot-Restart를 이용하도록 하자) 다음과 같은 화면이 출력된다.

- 네트워크 상의 이미지 넣기
- image.network를 사용해서 이미지를 넣을 수 있다.
body: Center( child: Image.network( 'https://w7.pngwing.com/pngs/476/26/png-transparent-airplane-paper-plane-drawing-paper-plane-angle-rectangle-triangle-thumbnail.png')));
이미지 링크는 마우스 ‘우클릭’ 후 ‘이미지 링크 복사’ 링크를 누르면 가져올 수 있다.
다음과 같은 화면이 출력된다.

- 이미지 수정
image 위젯에서는 크기를 설정할 수 있다. 너비와 높이를 100으로 설정해보자.
코드를 확인해보면 이미지 위젯 내에서 넓이와 높이를 조정하고 있다.
body: Center( child: Image.network( 'https://w7.pngwing.com/pngs/476/26/png-transparent-airplane-paper-plane-drawing-paper-plane-angle-rectangle-triangle-thumbnail.png' , width: 100, height: 100)));
다음과 같은 화면이 출력된다.

만약 입력하는 비율이 다르면 아무일도 일어나지 않는다.
예시로 위에서는 정사각형 이미지였기 때문에 100X100 사이즈로 변환이 되었지만, 100X300 사이즈로 변환한다면 아무일도 일어나지 않는다. 원본 이미지 비율과 설정한 비율이 다를 경우 원본 이미지를 지키는 방향으로 결과를 출력시킨다.
3. Text: 텍스트 위젯
- Textstyle 위젯 사용
텍스트 위젯은 Textstyle 위젯을 통해서 문자열을 꾸밀 수 있다. Textstyle 위젯에서는 폰트의 크기, 색상들을 바꿀 수 있고, 이렇게 작성된 Textstyle 위젯을 style 프로퍼티로 전달해 Text 스타일을 적용할 수 있다.
텍스트 색상을 보라색으로 하고, 크기를 좀 더 키우고 싶다면 밑처럼 작성하면 된다. 밑 코드에서는 Text 위젯과 TextStyle 위젯을 이용하였다. 대소문자 구분을 잘해서 코드를 작성하도록 하자.
body: Center( child: Text('Hello, text Widget!', style: TextStyle(fontSize: 25, color: Colors.purple))));
다음과 같은 화면이 출력된다.

- Text의 폰트 및 색상 바꾸기
폰트는 이미지 적용할 때와 방식이 비슷하다.
- 먼저 fonts 디렉토리를 만들어서 원하는 폰트를 다운로드 후, 저장한다.

나는 시인과 나DX 폰트를 받아서 저장했다. 폰트는 ttf 파일이어야 한다.
- pubspec.yaml 파일에 가서 폰트를 등록한다.
fonts: - family: DX fonts: - asset: fonts/DXPnM-KSCpc-EUC-H.ttf # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700
주석 처리가 기본으로 되어 있으므로 주석을 지우고 수정해주어야 한다.
font는 기본적으로 family로 묶인다. 시인과 나 폰트의 경우 한 가지의 종류이지만, 다른 폰트들은 Thin부터 Bold 스타일까지 다양한 폰트 파일들로 구성되어 있기 때문이다. 스타일만 다르고 종류는 같은 폰트들이 family로 묶이게 된다.
밑에 주석 처리 되어있는 부분을 보면, weight 라는 키워드가 있는데, 이는 폰트 굵기로 구분 짓겠다는 의미이다.
family 이름은 내가 편한대로 설정해도 된다. asset에는 ttf 파일 이름을 적는다. 이 때 띄어쓰기를 주의하도록 하자.
child: Text('Hello, text Widget!', style: TextStyle( fontSize: 25, color: Colors.blue, fontFamily: 'DX'))))
이후 Text 위젯에 폰트를 적용하면, 다음과 같은 화면이 나오게 된다.

추가로 fontWeight: FontWeight.w700 프로퍼티를 넣어주면 텍스트가 bold체로 바뀌게 된다.

4. Column: 위젯을 위에서 아래로 그려보자
먼저 위젯을 배치할 때 사용하는 레이아웃 형태의 위젯은 대표적으로 Column, Row. ListView, Stack 이렇게 4가지가 있다.
Column은 문자 그대로 위젯들을 위에서 아래로 배치하는 위젯이다. Column 여러 위젯들을 감싸기 때문에 프로퍼티로 children을 사용한다.
코드와 출력되는 화면은 다음과 같다. 괄호 갯수가 헷갈리니 주의하도록 하자.
body: Column(children: [ Text('Hello, text Widget!', style: TextStyle( fontSize: 25, color: Colors.blue, fontFamily: 'DX', fontWeight: FontWeight.w700)), Text('Hello, text Widget!', style: TextStyle( fontSize: 25, color: Colors.blue, fontFamily: 'DX', fontWeight: FontWeight.w700)), Text('Hello, text Widget!', style: TextStyle( fontSize: 25, color: Colors.blue, fontFamily: 'DX', fontWeight: FontWeight.w700)) ]));

전체 위젯들이 좌측 상단에 모여있는 것을 확인할 수 있다.
- Column 위젯의 프로퍼티
위 출력 결과를 보면 위젯들이 좌측 상단에 모여 있는 것을 확인 할 수 있다.
정가운데에 배치하기 위해서 우리는 mainAxisAlignment와 corssAxisAlignment 프로퍼티를 사용할 수 있다.
위에서 아래로 가는 방향을 main 왼쪽에서 오른쪽으로 가는 방향을 cross라고 이해하면 된다.
mainAxisAlignment: MainAxisAlignment.center
mainAxisAlignment: MainAxisAlignment.center 설정을 하면 상단에 붙어 있던 위젯들이 가운데로 내려온다.

위의 mainAxis 프로퍼티를 지우고
crossAxisAlignment: CrossAxisAlignment.start 설정을 하면 상단에 붙어있는 위젯들이 다시 위로 올라간다.
전체 위젯이 화면 가운데로 이동하지 못하는 이유는 무엇일까?
column의 너비는 화면 전체가 아닌 자식들만 감쌀 수 있을 만큼의 크기이기 때문이다.
이를 해결하기 위해서는 center위젯을 활용하면 된다.
center는 부모 위젯의 전체 크기에 의존하기 때문에, 화면 전체가 부모 위젯인 위와 같은 경우에서 가운데 배치가 될 수 있다.
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Color.fromARGB(255, 169, 58, 233)), useMaterial3: true, ), home: MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatelessWidget { final String title; MyHomePage({required this.title}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( title, ), ), body: Center( child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Hello, text Widget!', style: TextStyle( fontSize: 25, color: Colors.blue, fontFamily: 'DX', fontWeight: FontWeight.w700)), Text('Hello, text Widget!', style: TextStyle( fontSize: 25, color: Colors.blue, fontFamily: 'DX', fontWeight: FontWeight.w700)), Text('Hello, text Widget!', style: TextStyle( fontSize: 25, color: Colors.blue, fontFamily: 'DX', fontWeight: FontWeight.w700)) ]))); } }

5. Rows: 위젯을 왼쪽에서 오른쪽으로 그려보자
Column 위젯과 마찬가지의 특징을 가지고 있다.
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Color.fromARGB(255, 169, 58, 233)), useMaterial3: true, ), home: MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatelessWidget { final String title; MyHomePage({required this.title}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( title, ), ), body: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Hello, text Widget!', style: TextStyle( fontSize: 25, color: Colors.blue, fontFamily: 'DX', fontWeight: FontWeight.w700)), Text('Hello, text Widget!', style: TextStyle( fontSize: 25, color: Colors.blue, fontFamily: 'DX', fontWeight: FontWeight.w700)), Text('Hello, text Widget!', style: TextStyle( fontSize: 25, color: Colors.blue, fontFamily: 'DX', fontWeight: FontWeight.w700)) ])); } }

- Row 위젯의 프로퍼티
mainAxisAlignment: MainAxisAlignment.center를 입력하면 가운데 정렬이 된다.
정가운데 정렬을 하고 싶으면 column에서와 마찬가지로 Center위젯을 이용하면 된다.
mainAxisAlignment: MainAxisAlignment에는 Center를 제외하고도 다양한 성질이 있다. 이는 Column에서도 마찬가지로 적용된다.
mainAxisAlignment: MainAxisAlignment.spaceAround : 여백 생성
mainAxisAlignment: MainAxisAlignment.spaceBetween : 위젯들을 시작점과 끝점에 배치
mainAxisAlignment: MainAxisAlignment : 양 옆과 위젯 사이의 공백 모두 같게 생성
마지막꺼만 대표로 출력시켜보자.
결과는 다음과 같다.

6. ListView: 스크롤 할 수 있는 화면 만들기
Column과 Row로 화면 밖을 벗어나는 위젯을 만들려고 하면 오류가 생긴다.
Bottom Overflowed라는 메세지가 나타나면서 경고 표시를 출력해준다.
스크롤이 가능하도록 화면을 구성하려면 ListView를 사용하면 된다.
다음과 같이 코드를 작성하면 스크롤이 가능한 화면이 출력된다.
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Color.fromARGB(255, 169, 58, 233)), useMaterial3: true, ), home: MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatelessWidget { final String title; List<Widget> myChildren = []; MyHomePage({required this.title}); @override Widget build(BuildContext context) { for (var i = 0; i < 50; i++) { myChildren.add(Text('Hello, World!', style: TextStyle(fontSize: 25))); } return Scaffold( appBar: AppBar( title: Text( this.title, ), ), body: Center( child: ListView( children: myChildren, ), ), ); } }

- ListView 위젯의 프로퍼티
ListView는 좌우 방향으로도 설정할 수 있다.
scrollDirection 프로퍼티를 사용하게 되면 손쉽게 방향을 설정할 수 있게 된다.
class MyHomePage extends StatelessWidget { final String title; List<Widget> myChildren = []; MyHomePage({required this.title}); @override Widget build(BuildContext context) { for (var i = 0; i < 50; i++) { myChildren.add(Text('Hello, World!', style: TextStyle(fontSize: 25))); } return Scaffold( appBar: AppBar( title: Text( this.title, ), ), body: Center( child: ListView( scrollDirection: Axis.horizontal, children: myChildren, ), ), ); } }
- Builder
위에서는 ListView의 children에 리스트로 된 위젯 목록을 전달하는 방법으로 ListView를 선언했다.
다른 방법으로는 builder라는 함수를 이용해 ListView를 만드는 방법이 있다.
기존의 myChildren을 제거하고 ListView.builder라는 위젯의 형태로 만들었다.
class MyHomePage extends StatelessWidget { final String title; MyHomePage({required this.title}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( this.title, ), ), body: Center( child: ListView.builder( itemCount: 50, itemBuilder: (BuildContext context, int index) { return Text('$index' + 'Text', style: TextStyle(fontSize: 25)); }, ), ), ); } }
7. Stack: 위젯에 위젯 쌓기
Stack은 말 그대로 위젯 위에 위젯을 쌓을 때 사용하는 위젯이다.
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Color.fromARGB(255, 169, 58, 233)), useMaterial3: true, ), home: MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatelessWidget { final String title; MyHomePage({required this.title}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( this.title, ), ), body: Center( child: Stack(children: [ Image.network( 'https://w7.pngwing.com/pngs/476/26/png-transparent-airplane-paper-plane-drawing-paper-plane-angle-rectangle-triangle-thumbnail.png', height: 300, width: 300, ), Image.network( 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQJ29wXHfsDTnp5C4thR8krQuFZNzmOEZB0aY16dFnfGA&s', height: 300, width: 300, ), ]), ), ); } }

Stack은 children 내에 선언한 순서대로 쌓인다 (첫 번째 종이 비행기 위젯 위에 두 번째 구름 위젯이 쌓인 것을 확인할 수 있다).
- 정밀한 위젯 배치, Positioned
Stack을 이용해서 좀 더 정밀한 배치를 구현하려면 Positioned 위젯을 이용해야 한다.
아래는 좌측 방향으로 0만큼, 하단 방향으로 0만큼 떨어지게끔 배치하라는 설정이다.
class MyHomePage extends StatelessWidget { final String title; MyHomePage({required this.title}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( this.title, ), ), body: Center( child: Stack(children: [ Image.network( 'https://w7.pngwing.com/pngs/476/26/png-transparent-airplane-paper-plane-drawing-paper-plane-angle-rectangle-triangle-thumbnail.png', ), Positioned( left: 0, bottom: 0, child: Image.network( 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQJ29wXHfsDTnp5C4thR8krQuFZNzmOEZB0aY16dFnfGA&s', height: 150, width: 150, ), ) ]), ), ); } }

8. Button: TextButton, ElavateButton, OutlinedButton
버튼은 이벤트를 발생시키는 장치이다.
플러터에는 TextButton, Elevated Button, OutlinedButton, IconButton 4가지 종류의 버튼이 있다.
- Text Button
가장 간단한 형태의 버튼이다. 테두리도 없고, 안에 Text 위젯만 존재한다.
- Elevated Button
배경색이 칠해져 있는 상태의 버튼이다.
- Outlined Button
테두리가 그려져 있는 버튼이다.
- Icon Button
아이콘 모양 버튼이다. 위의 세 가지 버튼과는 다르게 Icon()을 인자로 받아 버튼을 구성한다.
9. Others..
이 외에도 다양한 위젯들이 있다.
다른 위젯들은 밑의 링크에서 확인할 수 있다.
이곳에는 위젯이 패키지 형태로 배포되어 있다. 하지만 누구나 위젯을 패키지로 만들어 올릴 수 있으므로 모든 위젯이 믿을만 하다고 할 수 없다. 신뢰할만한 위젯을 사용하도록 하자.
++ 플러터 공식 유튜브 채널을 통해 위젯을 활용할 수도 있다.
화면 전환 구현
1. 두 개 화면 구현하기
화면 전환을 구현하려면 먼저 두 개의 화면이 필요하다.
lib 폴더 내에 screen 폴더를 만들고, 그 안에 두 개의 dart 폴더를 만들자.

이후에는 두 파일에 내용을 작성하자.
두 파일은 화면 단위 위젯 이므로 Scaffold로 시작한다.
first_screen.dart
import 'package:flutter/material.dart'; class FirstScreen extends StatelessWidget { @override Widget build(BuildContext contect) { return Scaffold( appBar: AppBar(), body: Center( child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Text('This is First Screen'), ElevatedButton(onPressed: () {}, child: Text('Go to screen')) ]))); } }
second_screen.dart
import 'package:flutter/material.dart'; class SecondScreen extends StatelessWidget { @override Widget build(BuildContext contect) { return Scaffold( appBar: AppBar(), body: Center( child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Text('This is Second Screen'), OutlinedButton(onPressed: () {}, child: Text('Go to screen')) ]))); } }
그럼 이제 main 파일을 수정해보자.
기존에 사용하던 MyHomePage는 사용하지 않을 것이므로 이 클래스는 지워주었다. 앞서 작성한 FirstScreen을 import하고 home 프로퍼티에 Firscreen을 넣어주었다.
실행 결과는 밑과 같다.

지금까지 화면 두 개를 구성해 보고 첫 번째 화면을 device에 나타내어 보았다. 이제부터 본격적으로 화면 전환을 구현해 보자.
2. 화면 이동 (Navigator.push)
모든 화면 전환은 Navigator라는 위젯을 통해서 처리된다.
Navigator를 활용하면 화면을 특정 화면으로 이동하거나., 이전 화면으로 돌아가는 등의 구현을 할 수 있다.
우리는 ‘Go to screen’ 버튼을 눌렀을 때 화면 이동이 발생하도록 코딩할 것이다.
아까 만든 firstscreen에서 onPressed가 비워져 있는데, 이 부분을 채워볼 것이다.
수정한 코드는 밑과 같다.
import 'package:flutter/material.dart'; import 'package:flutter_application_2/screen/Second_screen.dart'; class FirstScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: Center( child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Text('This is First Screen'), ElevatedButton( onPressed: () { Navigator.of(context).push(MaterialPageRoute( builder: (BuildContext context) => SecondScreen())); }, child: Text('Go to screen')) ]))); } }
OnPressed는 Navigator.of(context)로 시작한다. 이는 현재 위젯인 Scanffold 화면으로 이동하겠다는 뜻이다.
화면 이동은 push로 구현한다. push는 말 그대로 화면을 쌓겠다는 의미이다.
또한 MeterialPageRoute를 통해 Nevigator가 이동할 경로를 지정한다. MeterialPageRoute에는 새로운 화면인 SecondScreen을 빌드하는 방식으로 새 화면으로 이동하겠다는 내용을 작성한다.
밑 출력 사진을 보면 화살표 모양으로 뒤로 가기 버튼이 생긴 걸 확인할 수 있다. 이는 scaffold의 appBar에 적용되어 있는 기본 기능이다. 뒤로 가기 버튼을 누르면 Go to Screen을 누르기 전 화면으로 (First Screen) 이동하게 된다.

3. 화면 뒤로 가기 (Nevigator.pop)
화면 가운데에 뒤로 가기 버튼을 한 번 구현해보자.
이번에는 second_screen 파일을 수정해야 한다. Navigator.of(context)까지는 동일하지만 그 뒤에는 pop()만 오면 된다.
import 'package:flutter/material.dart'; class SecondScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: Center( child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Text('This is Second Screen'), OutlinedButton( onPressed: () { Navigator.of(context).pop(); }, child: Text('Go to First Screen')) ]))); } }
출력 결과는 다음과 같다. Go to First Screen 버튼을 누르면 첫 번째 화면으로 돌아오는 걸 확인 할 수 있다.

4. 화면 이동할 때 데이터 전달하기
120p
[실습]
Qwerty 키보드 구현
숫자 키보드 구현
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: KeyboardDemo(), ); } } class KeyboardDemo extends StatefulWidget { @override _KeyboardDemoState createState() => _KeyboardDemoState(); } class _KeyboardDemoState extends State<KeyboardDemo> { String _inputText = ""; void _onKeyPress(String keyValue) { setState(() { _inputText += keyValue; }); } void _onDelete() { setState(() { if (_inputText.isNotEmpty) { _inputText = _inputText.substring(0, _inputText.length - 1); } }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Custom Keyboard Demo'), ), body: Column( children: [ Expanded( child: Container( color: Colors.grey[300], alignment: Alignment.center, child: Text( _inputText, style: TextStyle(fontSize: 24.0), ), ), ), Container( color: Colors.grey[400], child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ KeyboardButton( label: '1', onPressed: () => _onKeyPress('1'), ), KeyboardButton( label: '2', onPressed: () => _onKeyPress('2'), ), KeyboardButton( label: '3', onPressed: () => _onKeyPress('3'), ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ KeyboardButton( label: '4', onPressed: () => _onKeyPress('4'), ), KeyboardButton( label: '5', onPressed: () => _onKeyPress('5'), ), KeyboardButton( label: '6', onPressed: () => _onKeyPress('6'), ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ KeyboardButton( label: '7', onPressed: () => _onKeyPress('7'), ), KeyboardButton( label: '8', onPressed: () => _onKeyPress('8'), ), KeyboardButton( label: '9', onPressed: () => _onKeyPress('9'), ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ KeyboardButton( label: '0', onPressed: () => _onKeyPress('0'), ), KeyboardButton( label: '<', onPressed: _onDelete, ), ], ), ], ), ), ], ), ); } } class KeyboardButton extends StatelessWidget { final String label; final VoidCallback onPressed; KeyboardButton({required this.label, required this.onPressed}); @override Widget build(BuildContext context) { return ElevatedButton( onPressed: onPressed, child: Text( label, style: TextStyle(fontSize: 24.0), ), ); } }
영어 키보드 구현
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: KeyboardDemo(), ); } } class KeyboardDemo extends StatefulWidget { @override _KeyboardDemoState createState() => _KeyboardDemoState(); } class _KeyboardDemoState extends State<KeyboardDemo> { String _inputText = ""; void _onKeyPress(String keyValue) { setState(() { _inputText += keyValue; }); } void _onDelete() { setState(() { if (_inputText.isNotEmpty) { _inputText = _inputText.substring(0, _inputText.length - 1); } }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('English Qwerty Keyboard Demo'), ), body: Column( children: [ Expanded( child: Container( color: Colors.grey[300], alignment: Alignment.center, child: Text( _inputText, style: TextStyle(fontSize: 20.0), ), ), ), Container( color: Colors.grey[400], child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ KeyboardButton( label: 'Q', onPressed: () => _onKeyPress('Q'), ), KeyboardButton( label: 'W', onPressed: () => _onKeyPress('W'), ), KeyboardButton( label: 'E', onPressed: () => _onKeyPress('E'), ), KeyboardButton( label: 'R', onPressed: () => _onKeyPress('R'), ), KeyboardButton( label: 'T', onPressed: () => _onKeyPress('T'), ), KeyboardButton( label: 'Y', onPressed: () => _onKeyPress('Y'), ), KeyboardButton( label: 'U', onPressed: () => _onKeyPress('U'), ), KeyboardButton( label: 'I', onPressed: () => _onKeyPress('I'), ), KeyboardButton( label: 'O', onPressed: () => _onKeyPress('O'), ), KeyboardButton( label: 'P', onPressed: () => _onKeyPress('P'), ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ KeyboardButton( label: 'A', onPressed: () => _onKeyPress('A'), ), KeyboardButton( label: 'S', onPressed: () => _onKeyPress('S'), ), KeyboardButton( label: 'D', onPressed: () => _onKeyPress('D'), ), KeyboardButton( label: 'F', onPressed: () => _onKeyPress('F'), ), KeyboardButton( label: 'G', onPressed: () => _onKeyPress('G'), ), KeyboardButton( label: 'H', onPressed: () => _onKeyPress('H'), ), KeyboardButton( label: 'J', onPressed: () => _onKeyPress('J'), ), KeyboardButton( label: 'K', onPressed: () => _onKeyPress('K'), ), KeyboardButton( label: 'L', onPressed: () => _onKeyPress('L'), ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ KeyboardButton( label: 'Z', onPressed: () => _onKeyPress('Z'), ), KeyboardButton( label: 'X', onPressed: () => _onKeyPress('X'), ), KeyboardButton( label: 'C', onPressed: () => _onKeyPress('C'), ), KeyboardButton( label: 'V', onPressed: () => _onKeyPress('V'), ), KeyboardButton( label: 'B', onPressed: () => _onKeyPress('B'), ), KeyboardButton( label: 'N', onPressed: () => _onKeyPress('N'), ), KeyboardButton( label: 'M', onPressed: () => _onKeyPress('M'), ), KeyboardButton( label: '<', onPressed: _onDelete, ), ], ), ], ), ), ], ), ); } } class KeyboardButton extends StatelessWidget { final String label; final VoidCallback onPressed; KeyboardButton({required this.label, required this.onPressed}); @override Widget build(BuildContext context) { return ElevatedButton( onPressed: onPressed, child: Text( label, style: TextStyle(fontSize: 24.0), ), ); } }
한글 키보드 구현
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: KeyboardDemo(), ); } } class KeyboardDemo extends StatefulWidget { @override _KeyboardDemoState createState() => _KeyboardDemoState(); } class _KeyboardDemoState extends State<KeyboardDemo> { String _inputText = ""; void _onKeyPress(String keyValue) { setState(() { _inputText += keyValue; }); } void _onDelete() { setState(() { if (_inputText.isNotEmpty) { _inputText = _inputText.substring(0, _inputText.length - 1); } }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Korean Qwerty Keyboard Demo'), ), body: Column( children: [ Expanded( child: Container( color: Colors.grey[300], height: 700, alignment: Alignment.center, child: Text( _inputText, style: TextStyle(fontSize: 20.0), ), ), ), SizedBox(height: 7), // 행과 행 사이의 간격을 조절하는 SizedBox 추가 Container( color: Colors.grey[400], child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ KeyboardButton( label: 'ㅂ', onPressed: () => _onKeyPress('ㅂ'), ), KeyboardButton( label: 'ㅈ', onPressed: () => _onKeyPress('ㅈ'), ), KeyboardButton( label: 'ㄷ', onPressed: () => _onKeyPress('ㄷ'), ), KeyboardButton( label: 'ㄱ', onPressed: () => _onKeyPress('ㄱ'), ), KeyboardButton( label: 'ㅅ', onPressed: () => _onKeyPress('ㅅ'), ), KeyboardButton( label: 'ㅛ', onPressed: () => _onKeyPress('ㅛ'), ), KeyboardButton( label: 'ㅕ', onPressed: () => _onKeyPress('ㅕ'), ), KeyboardButton( label: 'ㅑ', onPressed: () => _onKeyPress('ㅑ'), ), KeyboardButton( label: 'ㅐ', onPressed: () => _onKeyPress('ㅐ'), ), KeyboardButton( label: 'ㅔ', onPressed: () => _onKeyPress('ㅔ'), ), ], ), SizedBox(height: 7), // 행과 행 사이의 간격을 조절하는 SizedBox 추가 Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ KeyboardButton( label: 'ㅁ', onPressed: () => _onKeyPress('ㅁ'), ), KeyboardButton( label: 'ㄴ', onPressed: () => _onKeyPress('ㄴ'), ), KeyboardButton( label: 'ㅇ', onPressed: () => _onKeyPress('ㅇ'), ), KeyboardButton( label: 'ㄹ', onPressed: () => _onKeyPress('ㄹ'), ), KeyboardButton( label: 'ㅎ', onPressed: () => _onKeyPress('ㅎ'), ), KeyboardButton( label: 'ㅗ', onPressed: () => _onKeyPress('ㅗ'), ), KeyboardButton( label: 'ㅓ', onPressed: () => _onKeyPress('ㅓ'), ), KeyboardButton( label: 'ㅏ', onPressed: () => _onKeyPress('ㅏ'), ), KeyboardButton( label: 'ㅣ', onPressed: () => _onKeyPress('ㅣ'), ), ], ), SizedBox(height: 7), // 행과 행 사이의 간격을 조절하는 SizedBox 추가 Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ SizedBox( width: 45.0, // 원하는 너비로 조정 height: 45.0, // 원하는 높이로 조정 child: KeyboardButton( label: '|', onPressed: () => _onKeyPress('|'), ), ), KeyboardButton( label: 'ㅋ', onPressed: () => _onKeyPress('ㅋ'), ), KeyboardButton( label: 'ㅌ', onPressed: () => _onKeyPress('ㅌ'), ), KeyboardButton( label: 'ㅊ', onPressed: () => _onKeyPress('ㅊ'), ), KeyboardButton( label: 'ㅍ', onPressed: () => _onKeyPress('ㅍ'), ), KeyboardButton( label: 'ㅠ', onPressed: () => _onKeyPress('ㅠ'), ), KeyboardButton( label: 'ㅜ', onPressed: () => _onKeyPress('ㅜ'), ), KeyboardButton( label: 'ㅡ', onPressed: () => _onKeyPress('ㅡ'), ), SizedBox( width: 45.0, // 원하는 너비로 조정 height: 45.0, // 원하는 높이로 조정 child: KeyboardButton( label: '<-', onPressed: _onDelete, ), ), ], ), SizedBox(height: 7), // 행과 행 사이의 간격을 조절하는 SizedBox 추가 Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ SizedBox( width: 45.0, // 원하는 너비로 조정 height: 45.0, // 원하는 높이로 조정 child: KeyboardButton( label: '123', onPressed: () => _onKeyPress('123'), ), ), SizedBox( width: 45.0, // 원하는 너비로 조정 height: 45.0, // 원하는 높이로 조정 child: KeyboardButton( label: ':)', onPressed: () => _onKeyPress(':)'), ), ), SizedBox( width: 200.0, // 원하는 너비로 조정 height: 45.0, // 원하는 높이로 조정 child: KeyboardButton( label: 'space', onPressed: () => _onKeyPress(' '), ), ), SizedBox( width: 90.0, // 원하는 너비로 조정 height: 45.0, // 원하는 높이로 조정 child: KeyboardButton( label: 'enter', onPressed: () => _onKeyPress('\n'), ), ), ], ), SizedBox(height: 7), ], ), ), ], ), ); } } class KeyboardButton extends StatelessWidget { final String label; final VoidCallback onPressed; KeyboardButton({required this.label, required this.onPressed}); @override Widget build(BuildContext context) { return SizedBox( width: 35.0, // 원하는 너비로 조정 height: 45.0, // 원하는 높이로 조정 child: ElevatedButton( onPressed: onPressed, child: Text( label, style: TextStyle(fontSize: 15.0), // 원하는 글꼴 크기로 조정 ), ), ); } }

특수 기호 키보드 구현
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: KeyboardDemo(), ); } } class KeyboardDemo extends StatefulWidget { @override _KeyboardDemoState createState() => _KeyboardDemoState(); } class _KeyboardDemoState extends State<KeyboardDemo> { String _inputText = ""; void _onKeyPress(String keyValue) { setState(() { _inputText += keyValue; }); } void _onDelete() { setState(() { if (_inputText.isNotEmpty) { _inputText = _inputText.substring(0, _inputText.length - 1); } }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Korean Qwerty Keyboard Demo'), ), body: Column( children: [ Expanded( child: Container( color: Colors.grey[300], height: 700, alignment: Alignment.center, child: Text( _inputText, style: TextStyle(fontSize: 20.0), ), ), ), SizedBox(height: 7), // 행과 행 사이의 간격을 조절하는 SizedBox 추가 Container( color: Colors.grey[400], child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ KeyboardButton( label: '1', onPressed: () => _onKeyPress('1'), ), KeyboardButton( label: '2', onPressed: () => _onKeyPress('2'), ), KeyboardButton( label: '3', onPressed: () => _onKeyPress('3'), ), KeyboardButton( label: '4', onPressed: () => _onKeyPress('4'), ), KeyboardButton( label: '5', onPressed: () => _onKeyPress('5'), ), KeyboardButton( label: '6', onPressed: () => _onKeyPress('6'), ), KeyboardButton( label: '7', onPressed: () => _onKeyPress('7'), ), KeyboardButton( label: '8', onPressed: () => _onKeyPress('8'), ), KeyboardButton( label: '9', onPressed: () => _onKeyPress('9'), ), KeyboardButton( label: '0', onPressed: () => _onKeyPress('0'), ), ], ), SizedBox(height: 7), // 행과 행 사이의 간격을 조절하는 SizedBox 추가 Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ KeyboardButton( label: '-', onPressed: () => _onKeyPress('-'), ), KeyboardButton( label: '/', onPressed: () => _onKeyPress('/'), ), KeyboardButton( label: ':', onPressed: () => _onKeyPress(':'), ), KeyboardButton( label: ';', onPressed: () => _onKeyPress(';'), ), KeyboardButton( label: '(', onPressed: () => _onKeyPress('('), ), KeyboardButton( label: ')', onPressed: () => _onKeyPress(')'), ), KeyboardButton( label: "\$", onPressed: () => _onKeyPress('\$'), ), KeyboardButton( label: '&', onPressed: () => _onKeyPress('&'), ), KeyboardButton( label: '@', onPressed: () => _onKeyPress('@'), ), KeyboardButton( label: '"', onPressed: () => _onKeyPress('"'), ), ], ), SizedBox(height: 7), // 행과 행 사이의 간격을 조절하는 SizedBox 추가 Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ SizedBox( width: 45.0, // 원하는 너비로 조정 height: 45.0, // 원하는 높이로 조정 child: KeyboardButton( label: '#+=', onPressed: () => _onKeyPress('|'), ), ), SizedBox( width: 50.0, // 원하는 너비로 조정 height: 45.0, // 원하는 높이로 조정 child: KeyboardButton( label: ".", onPressed: () => _onKeyPress("."), ), ), SizedBox( width: 50.0, // 원하는 너비로 조정 height: 45.0, // 원하는 높이로 조정 child: KeyboardButton( label: ",", onPressed: () => _onKeyPress(","), ), ), SizedBox( width: 50.0, // 원하는 너비로 조정 height: 45.0, // 원하는 높이로 조정 child: KeyboardButton( label: "?", onPressed: () => _onKeyPress("?"), ), ), SizedBox( width: 50.0, // 원하는 너비로 조정 height: 45.0, // 원하는 높이로 조정 child: KeyboardButton( label: "!", onPressed: () => _onKeyPress("!"), ), ), SizedBox( width: 50.0, // 원하는 너비로 조정 height: 45.0, // 원하는 높이로 조정 child: KeyboardButton( label: "'", onPressed: () => _onKeyPress("'"), ), ), SizedBox( width: 45.0, // 원하는 너비로 조정 height: 45.0, // 원하는 높이로 조정 child: KeyboardButton( label: '<-', onPressed: _onDelete, ), ), ], ), SizedBox(height: 7), // 행과 행 사이의 간격을 조절하는 SizedBox 추가 Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ SizedBox( width: 45.0, // 원하는 너비로 조정 height: 45.0, // 원하는 높이로 조정 child: KeyboardButton( label: '123', onPressed: () => _onKeyPress('123'), ), ), SizedBox( width: 45.0, // 원하는 너비로 조정 height: 45.0, // 원하는 높이로 조정 child: KeyboardButton( label: ':)', onPressed: () => _onKeyPress(':)'), ), ), SizedBox( width: 200.0, // 원하는 너비로 조정 height: 45.0, // 원하는 높이로 조정 child: KeyboardButton( label: 'space', onPressed: () => _onKeyPress(' '), ), ), SizedBox( width: 90.0, // 원하는 너비로 조정 height: 45.0, // 원하는 높이로 조정 child: KeyboardButton( label: 'enter', onPressed: () => _onKeyPress('\n'), ), ), ], ), SizedBox(height: 7), ], ), ), ], ), ); } } class KeyboardButton extends StatelessWidget { final String label; final VoidCallback onPressed; KeyboardButton({required this.label, required this.onPressed}); @override Widget build(BuildContext context) { return SizedBox( width: 35.0, // 원하는 너비로 조정 height: 45.0, // 원하는 높이로 조정 child: ElevatedButton( onPressed: onPressed, child: Text( label, style: TextStyle(fontSize: 15.0), // 원하는 글꼴 크기로 조정 ), ), ); } }



