이 글은 골든래빗 《코드팩토리의 플러터 프로그래밍》의 스터디 내용 입니다.
void main() {
(String, int) minji = ('민지', 20);
print(minji); // (민지, 20)
print(minji.$1); // 민지
}
포지셔널 파라미터와 다르게 입력 순서를 지킬 필요가 없다.
void main() {
({String name, int age}) minji = (
name: '민지',
age: 20,
);
print(minji); // (age: 20, name: 민지)
}
👩🏻💻 왜 인덱스가 1부터 시작하지 않을까? 찾아보다가 더 요상한 것도 알게 되었다. 네임드와 포지셔널 파라미터를 섞어쓸 수 있으며, $<position>
접근 방식은 네임드 파라미터를 건너뛴다는 것이다. 안타깝게도 인덱스가 왜 1부터 시작인지는 찾을 수가 없었다.
var record = ('first', a: 2, b: true, 'last');
print(record.$1); // Prints 'first'
print(record.a); // Prints 2
print(record.b); // Prints true
print(record.$2); // Prints 'last'
반환된 타입을 그대로 복제해서 타입 내부에 각각의 값을 직접 추출해오는 문법
void main() {
final [minji, haerin] = ['민지', '해린'];
print(minji); // 민지
print(haerin); // 해린
}
void main() {
final numbers = [1, 2, 3, 4, 5, 6, 7, 8];
// 스프레드 연산자를 사용하면 중간의 필요없는 값들을 버릴 수 있다.
final [x, y, ..., z] = numbers;
print(x); // 1
print(y); // 2
print(z); // 8
}
void main() {
final minjiMap = {'name': '민지', 'age': 19};
final {'name': name, 'age': age} = minjiMap;
print('name: $name'); // name: 민지
print('age: $age'); // age: 19
}
void main() {
final minji = Idol(name: '민지', age: 19);
final Idol(name: name, age: age) = minji; // 생성자 구조와 똑같이 구조 분해
print(name); // 민지
print(age); // 19
}
class Idol {
final String name;
final int age;
Idol({
required this.name,
required this.age,
});
}
다트 3.0 부터는 switch문을 함수처럼 사용하여 직접 값을 반환받을 수 있는 절 기능이 추가되었다.
void main() {
String dayKor = '월요일';
String dayEnglish = switch (dayKor) {
'월요일' => 'Monday',
'화요일' => 'Tuesday',
'수요일' => 'Wednesday',
'목요일' => 'Thursday',
'금요일' => 'Friday',
'토요일' => 'Saturday',
'일요일' => 'Sunday',
_ => 'Not Found'
};
print(dayEnglish); // Monday
}
void switcher(dynamic anything) {
switch (anything) {
case 'aaa':
print('match: aaa');
break;
case [1, 2]:
print('match: [1, 2]');
break;
case [_, _, _]:
print('match [_,_,_]');
break;
case [int a, int b]:
print('match: [int $a, int $b]');
break;
case (String a, int b):
print('match: (String: $a, int: $b)');
break;
default:
print('no match');
}
}
void main() {
switcher('aaa'); // match: aaa
switcher([1, 2]); // match: [1, 2]
switcher([3, 4, 5]); // match [_,_,_]
switcher([6, 7]); // match: [int 6, int 7]
switcher(('민지', 19)); // match: (String: 민지, int: 19)
switcher(8); // no match
}
코드가 입력받을 수 있는 모든 조건을 전부 확인하고 있는지 체크하는 기술
void main() {
bool? val; // true, false, null이 될 수 있음
// null 조건을 입력하지 않았기 때문에 에러 발생
// null case를 추가하거나 default case를 추가해야 에러가 사라짐
switch (val) {
case true:
print('true');
case false:
print('false');
}
}
switch문에 when 키워드로 보호 구문guard clause을 추가할 수 있도록 업데이트 되었다.
void main() {
(int a, int b) val = (1, -1);
switch(val) {
case (1, _) when val.$2 > 0:
print('1, _');
break;
default:
print('default'); // default
}
}
class
키워드 앞에 명시한다.sealed class Vehicle {}
class Car extends Vehicle {}
class Truck implements Vehicle {}
class Bicycle extends Vehicle {}
String getVehicleSound(Vehicle vehicle) {
// ERROR: The switch is missing the Bicycle subtype or a default case.
return switch (vehicle) {
Car() => 'vroom',
Truck() => 'VROOOOMM',
};
}
이름 그대로 mixin 클래스이다. 이전에 class 키워드로 생성된 mixin 들은 더이상 mixin으로 쓰일 수 없다.
플러터가 스키아 엔진과 직접 통신한다 👉 어떤 플랫폼이든 스키아 엔진을 지원한다면 플러터가 컴파일되고 실행되도록 구현할 수 있다
플러터는 위젯을 스키아 엔진에 직접 그려내고 필요한 제스처 및 이벤트 브릿지를 통하지 않고 실행 👉 리액트 네이티브에 비해 상당히 빠른 퍼포먼스
안드로이드 스튜디오에서 [New Flutter Project]를 통해 프로젝트를 생성하면 다음과 같은 파일들이 생성된다.
lib/main.dart
파일을 수정하여 'Hello Code Factory'를 출력해보자.
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
body: Text(
'Hello Code Factory',
),
),
),
);
}
MaterialApp
: 머티리얼 디자인 기반의 위젯들을 사용하게 해주는 위젯Scaffold
: 화면 전체를 차지하며 레이아웃을 도와주고 UI 관련 특수 기능을 제공한다. ex) 스낵바, 앱바, 탭바시뮬레이터에서 위 코드를 실행해보자.
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
body: Center( // 👈 Center 위젯 추가
child: Text(
'Hello Code Factory',
),
),
),
),
);
}
Text를 변경 후 저장하면, 아래와 같은 로그를 볼 수 있지만 시뮬레이터 속 내용은 변하지 않는다.
Performing hot reload...
Syncing files to device sdk gphone64 x86 64...
Reloaded 1 of 696 libraries in 774ms (compile: 29 ms, reload: 392 ms, reassemble: 305 ms).
D/EGL_emulation( 4457): app_time_stats: avg=6604.28ms min=6604.28ms max=6604.28ms count=1
Flutter 문서 How to perform a hot reload에서 다음과 같은 내용을 확인할 수 있다.
A code change has a visible effect only if the modified Dart code is run again after the change. Specifically, a hot reload causes all the existing widgets to rebuild. Only code involved in the rebuilding of the widgets is automatically re-executed. The main() and initState() functions, for example, are not run again.
핫 리로드는 위젯의 build()를 확인하여 변경 사항을 적용한다는 것이다.
StatelessWidget를 상속해서 build() 메소드를 재정의해보았다.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget{
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Text(
'Hello Code Factory',
),
),
),
);
}
}
Hot Reload 성공! 😆
child
매개변수를 받음)children
매개변수를 받음)child와 children 매개변수를 동시에 입력받는 위젯은 존재하지 않는다.
(👩🏻💻 책 예제는 여전히 Hot Reload가 안되기 때문에 안드로이드 스튜디오의 Hot Restart를 사용해서 실습을 진행했다.)
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
body: Center(
child: Text(
'Code Factory', // 하나의 위젯만 가운데 정렬 가능
),
),
),
),
);
}
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: SizedBox(
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [ // 여러 위젯을 Column 위젯에 입력 가능
Text('Code'),
Text('Factory'),
],
)
),
),
),
);
}
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Text(
'코드팩토리', // 작성하고 싶은 글
style: TextStyle( // 글자에 스타일 적용
fontSize: 16.0, // 글자 크기
fontWeight: FontWeight.w700, // 글자 굵기
color: Colors.blue, // 글자 색상
),
),
),
),
);
}
}
사용자가 키보드로 글자를 입력하는 행위 외의 모든 입력을 제스처라고 부른다. 제스처 관련 위젯은 특정 제스처가 입력됐을 때 인지하고 콜백 함수를 실행한다.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: TextButton(
onPressed: () {}, // 클릭시 실행
style: TextButton.styleFrom( // 스타일 지정
foregroundColor: Colors.red, // 주색상 지정
),
child: Text('텍스트 버튼'), // 버튼에 넣을 위젯
),
),
),
);
}
}
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: OutlinedButton(
onPressed: () {}, // 클릭시 실행
style: OutlinedButton.styleFrom( // 버튼 스타일
foregroundColor: Colors.red,
),
child: Text('아웃라인드 버튼'), // 버튼에 넣을 위젯
),
),
),
);
}
}
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {}, // 클릭시 실행
style: ElevatedButton.styleFrom( // 버튼 스타일
backgroundColor: Colors.red,
),
child: Text('엘리베이티드 버튼'), // 버튼에 넣을 위젯
),
),
),
);
}
}
Icons 클래스를 통해 기본 제공 아이콘들을 사용할 수 있다. 제공되는 아이콘 목록은 https://fonts.google.com/icons에서 확인할 수 있다.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: IconButton(
onPressed: () {}, // 클릭시 실행
icon: Icon(
Icons.home,
)
),
),
),
);
}
}
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: GestureDetector(
onTap: () { // 한 번 탭했을 때 실행할 함수
print('on tap');
},
onDoubleTap: () { // 두 번 탭했을 때 실행할 함수
print('on double tap');
},
onLongPress: () {
print('on long press');
},
child: Container( // 제스처를 적용할 위젯
decoration: BoxDecoration(
color: Colors.red,
),
width: 100.0,
height: 100.0,
),
)
),
),
);
}
}
빨간 박스를 탭, 더블탭, 롱프레스 했을때 콘솔에서 아래와 같은 로그를 확인할 수 있다.
I/flutter ( 5420): on tap
I/flutter ( 5420): on double tap
I/flutter ( 5420): on long press
이 외에 onPanStart
, onPanUpdate
, onPanEnd
, onHorizontalDragStart
, onHorizontalDragUpdate
, onHorizontalDragEnd
, onVerticalDragStart
, onVerticalDragUpdate
, onVerticalDragEnd
, onScaleStart
, onScaleUpdate
, onScaleEnd
등이 있다. https://api.flutter.dev/flutter/widgets/GestureDetector-class.html에서 모두 확인 가능하다.
import 'package:flutter/material.dart';
void main() {
runApp(FloatingActionButtonExample());
}
class FloatingActionButtonExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {}, // 클릭했을 때 실행할 함수
child: Text('클릭'),
),
body: Container(),
),
);
}
}
다른 위젯을 담는 용도, 위젯의 너비와 높이를 지정하거나 배경이나 테두리를 추가할때 많이 사용한다.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Container(
decoration: BoxDecoration(
color: Colors.red, // 배경색 적용
border: Border.all(
// 테두리 적용
width: 16.0, // 테두리 굵기
color: Colors.black, // 테두리 색상
),
borderRadius: BorderRadius.circular(
16.0,
),
),
height: 200.0, // 높이
width: 100.0, // 너비
),
),
),
);
}
}
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: SizedBox(
height: 200.0, // 높이 지정
width: 200.0, // 너비 지정
child: Container(
// 크기 확인 용도로 Container 추가
color: Colors.red,
),
),
),
),
);
}
}
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Container(
color: Colors.blue,
child: Padding(
padding: EdgeInsets.all(
// 상하, 좌우로 모두 16픽셀만큼 패딩 적용
16.0,
),
child: Container(
color: Colors.red,
width: 50.0,
height: 50.0,
),
),
),
),
),
);
}
}
패딩은 적용된 위젯이 차지하는 크기 내부에서 간격이 추가된다. 위젯의 바깥에 간격을 추가해주는 마진margin이라는 기능도 있다. 따로 마진 위젯이 존재하지는 않고 Container 위젯에 추가할 수 있다.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
// 최상위 검정 컨테이너 (margin이 적용되는 대상)
child: Container(
color: Colors.black,
child: Container(
// 중간 파란 컨테이너
color: Colors.blue,
margin: EdgeInsets.all(16.0), // 마진 적용 위치
child: Padding(
// 패딩 적용
padding: EdgeInsets.all(16.0),
// 패딩이 적용된 빨간 컨테이너
child: Container(
color: Colors.red,
width: 50,
height: 50,
),
)),
),
),
),
);
}
}
👩🏻💻 바로 위 예제와 비교해보면 쉽게 마진의 역할을 이해할 수 있다. 검정색 컨테이너가 마진이 적용된 부분이다.
SafeArea 위젯을 사용하면 따로 기기별로 예외 처리를 하지 않고도 안전한(Safe) 화면에서만 위젯을 그릴 수 있다.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SafeArea(
top: true, // 적용
bottom: true,
left: true,
child: Container(
color: Colors.red,
height: 300.0,
width: 300.0,
),
),
),
);
}
}
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SafeArea(
top: true, // 미적용
bottom: true,
left: true,
child: Container(
color: Colors.red,
height: 300.0,
width: 300.0,
),
),
),
);
}
}
가로로 위젯을 배치하는 데 사용된다. 여러 개의 child 위젯을 입력받는 children 매개변수를 노출한다. 주축main axis과 반대축cross axis라는 개념이 있다. 아래 그림에서 회색 동그라미들이 children에 해당한다.
import 'package:flutter/material.dart';
void main() {
runApp(RowWidgetExample());
}
class RowWidgetExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SizedBox(
height: double.infinity,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
height: 50.0,
width: 50.0,
color: Colors.red,
),
const SizedBox(width: 12.0),
Container(
height: 50.0,
width: 50.0,
color: Colors.green,
),
const SizedBox(width: 12.0),
Container(
height: 50.0,
width: 50.0,
color: Colors.blue,
)
],
),
),
),
);
}
}
import 'package:flutter/material.dart';
void main() {
runApp(ColumnWidgetExample());
}
class ColumnWidgetExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SizedBox(
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
height: 50.0,
width: 50.0,
color: Colors.red,
),
const SizedBox(width: 12.0),
Container(
height: 50.0,
width: 50.0,
color: Colors.green,
),
const SizedBox(width: 12.0),
Container(
height: 50.0,
width: 50.0,
color: Colors.blue,
),
],
),
),
),
);
}
}
Row나 Column에서 사용하는 위젯이다. Flexible에 제공된 child가 크기를 최소한으로 차지하게 할 수 있다.
import 'package:flutter/material.dart';
void main() {
runApp(ColumnWidgetExample());
}
class ColumnWidgetExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SizedBox(
width: double.infinity,
child: Column(
children: [
Flexible(
flex: 1,
child: Container(
color: Colors.blue,
),
),
Flexible(
flex: 1,
child: Container(
color: Colors.red,
),
)
],
),
),
),
);
}
}
Flexible 위젯을 상속하는 위젯. 남아 있는 공간을 최대한으로 차지한다. Flexible 위젯과 다르게 남는 공간을 최대한으로 차지한다.
import 'package:flutter/material.dart';
void main() {
runApp(ColumnWidgetExample());
}
class ColumnWidgetExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SizedBox(
width: double.infinity,
child: Column(
children: [
Expanded(
flex: 1,
child: Container(
color: Colors.blue,
),
),
Expanded(
flex: 1,
child: Container(
color: Colors.red,
),
)
],
),
),
),
);
}
}
Expanded 위젯이 두 개이기 때문에 각 위젯이 남는 공간을 똑같이 나눠서 차지한다.
위젯을 겹치는 기능을 제공한다.
import 'package:flutter/material.dart';
void main() {
runApp(ColumnWidgetExample());
}
class ColumnWidgetExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SizedBox(
width: double.infinity,
child: Stack(
children: [
Container(
height: 300.0,
width: 300.0,
color: Colors.red,
),
Container(
height: 250.0,
width: 250.0,
color: Colors.yellow,
),
Container(
height: 200.0,
width: 200.0,
color: Colors.blue,
)
],
),
),
),
);
}
}
플러터 프로젝트 생성시 자동으로 생성되는 pubspec.yaml
파일에 원하는 플러그인을 추가하고 [pub get] 버튼을 눌러준다.
import 'package:flutter/material.dart';
void main() {
runApp(SplashScreen()); // SplashScreen 위젯을 첫 화면으로 지정
}
class SplashScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
// 중앙 정렬 위젯
body: Center(
// 글자를 화면에 보여주는 위젯
child: Text('Splash Screen'),
),
),
);
}
}
import 'package:flutter/material.dart';
void main() {
runApp(SplashScreen());
}
class SplashScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
// 컨테이너 위젯
body: Container(
// 컨테이너를 디자인하는 클래스
decoration: BoxDecoration(
color: Colors.orange,
),
child: Center(
child: Text('Splash Screen'),
),
),
),
);
}
}
프로젝트 최상위에 이미지를 담을 assets
폴더를 추가한다.
pubspec.yaml
파일에 assets
폴더를 추가한다.
flutter:
# ...
assets:
- assets/
# ...
[pub get] 버튼을 눌러 flutter pub get
을 실행한다.
코드 수정
import 'package:flutter/material.dart';
void main() {
runApp(SplashScreen());
}
class SplashScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Container(
decoration: BoxDecoration(
// 로고 배경과 같은 색으로 변경
color: Color(0XFFF99231),
),
child: Center(
// Text 위젯을 Image 위젯으로 변경
child: Image.asset(
'assets/logo.png',
),
),
),
),
);
}
}
import 'package:flutter/material.dart';
void main() {
runApp(SplashScreen());
}
class SplashScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Container(
decoration: BoxDecoration(
color: Color(0XFFF99231),
),
// Row 추가
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
// 가운데 정렬 추가
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/logo.png',
width: 200, // 너비 추가
),
// 로딩 애니메이션 추가
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(Colors.white),
),
],
),
],
),
),
),
);
}
}