이 글은 골든래빗 《코드팩토리의 플러터 프로그래밍》의 스터디 내용 입니다.
다트는 프로그램 시작점인 엔트리 함수 기호로 main()을 사용한다. void는 반환값이 없다는 의미이다.
void main() {
// 한 줄 주석은 이렇게
/*
* 여러 줄 주석은 이렇게
*/
/// 문서 주석은 이렇게 슬래시 3개로 작성한다.
// print 함수는 이렇게 생겼다
print('Hello World'); // 세미콜론을 꼭 찍어줘야 한다.
}
👩🏻💻 print()
함수는 newline을 끝에 포함한다. newline이 없으려면 stdout.write()
를 써야한다. 이 함수를 쓰려면 dart.io
를 import
해야한다.
var 변수명 = 값;
형식으로 선언한다.모든 변수는 고유의 변수 타입을 갖고 있다.
void main() {
// String - 문자열
String name = '코드팩토리';
// int - 정수
int isInt = 10;
// double - 실수
double isDouble = 2.5;
// bool - 불리언 (true/false)
bool isTrue = true;
}
리스트명[인덱스]
형식으로 특정 원소에 접근한다.void main() {
// 리스트에 넣을 타입을 <> 사이에 명시할 수 있다.
List<String> blackPinkList = ['리사', '지수', '제니', '로제'];
// length로 길이를 확인할 수 있다.
print(blackPinkList.length);
// add() 함수는 리스트의 끝에 원소를 추가할 수 있다.
blackPinkList.add('코드팩토리');
// where() 함수는 List에 있는 값들을 순서대로 순회하면서 특정 조건에 맞는 값만 필터링한다.
// 순회가 끝나면 유지된 값들을 기반으로 이터러블이 반환된다.
final newList = blackPinkList.where(
(name) => name == '리사' || name == '지수',
);
// toList() 함수는 Iterable을 List로 변환한다.
print(newList.toList()); // [리사, 지수]
// map() 함수는 List의 원소를 순서대로 순회하면서 값을 변경할 수 있다.
final newBlackPink = blackPinkList.map(
(name) => '블랙핑크 $name',
);
print(newBlackPink.toList()); // [블랙핑크 리사, 블랙핑크 지수, 블랙핑크 제니, 블랙핑크 로제]
// reduce() 함수는 List의 원소를 순회하며 매개변수에 입력된 함수를 실행한다.
// 순회할 때마다 값을 쌓아가는 특징이 있다.
// List 멤버의 타입과 같은 타입을 반환한다.
// 초기값을 넣을 수 없다.
final allMembers = blackPinkList.reduce((value, element) => value + ', ' + element);
print(allMembers); // 리사, 지수, 제니, 로제
// fold() 함수는 reduce() 함수와 똑같이 실행되지만 반환되는 타입에 제한이 없다.
final allMembers2 = blackPinkList.fold<int>(0, (value, element) => value + element.length);
print(allMembers2); // 8
}
Bad state: No element
에러가 나지만 fold는 초기값을 반환한다.list.reduce(f)
는 list.skip(1).fold(list.first, f)
의 shortcut으로 볼 수 있다.Map<키 타입, 값 타입> 맵이름
형식으로 생성void main() {
Map<String, String> dictionary = {
'Harry Potter': '해리 포터', // 키 : 값
'Ron Weasley': '론 위즐리',
'Hermione Granger': '헤르미온느 그레인저',
};
print(dictionary['Harry Potter']); // 해리 포터
print(dictionary['Hermione Granger']); // 헤르미온느 그레인저
// 키와 값 반환받기
print(dictionary.keys); // (Harry Potter, Ron Weasley, Hermione Granger)
print(dictionary.values); // (해리 포터, 론 위즐리, 헤르미온느 그레인저)
}
Set<타입> 세트이름
형식으로 생성void main() {
Set<String> blackPink = {'로제', '지수', '리사', '제니', '제니'}; // 제니 중복
print(blackPink); // {로제, 지수, 리사, 제니}
// contains() 함수로 값이 있는지 확인할 수 있다.
print(blackPink.contains('로제')); // true
}
enum Status {
approved,
pending,
rejected,
}
void main() {
Status status = Status.approved;
print(status); // Status.approved
}
void main() {
double number = 2;
print(number + 2); // 4.0
print(number - 2); // 0.0
print(number * 2); // 4.0
print(number / 2); // 1.0
print(number % 3); // 2.0
// 단항 연산도 가능
number++;
number--;
number += 2;
number -= 2;
number *= 2;
number /= 2;
}
?
를 추가해줘야 null 값이 저장될 수 있다.void main() {
// 타입 뒤에 ?를 명시해서 null값을 가질 수 있다.
double? number1 = null;
// 타입 뒤에 ?를 명시하지 않아 에러가 난다.
// The value 'null' can't be assigned to a variable of type 'double'
double number2 = null;
// ??를 사용하면 기존 값이 null일 때만 새 값이 할당된다.
number1 ??= 3;
print(number1); // 3.0
// null이 아니므로 3이 유지된다.
number1 ??= 4;
print(number1); // 3.0
}
void main() {
int number1 = 1;
int number2 = 2;
print(number1 > number2); // false
print(number1 < number2); // true
print(number1 >= number2); // false
print(number1 <= number2); // true
print(number1 == number2); // false
print(number1 != number2); // true
}
is
키워드를 사용하여 변수의 타입을 비교할 수 있다.
void main() {
int number1 = 1;
print(number1 is int); // true
print(number1 is String); // false
print(number1 is! int); // false
print(number1 is! String); // true
}
void main() {
bool result = 12 > 10 && 1 > 0;
print(result); // true
bool result2 = 12 > 10 && 0 > 1;
print(result2); // false
bool result3 = 12 > 10 || 1 > 0;
print(result3); // true
bool result4 = 12 > 10 || 0 > 1;
print(result4); // true
bool result5 = 12 < 10 || 0 > 1;
print(result5); // false
}
void main() {
int number = 2;
if (number % 3 == 0) {
print('3의 배수입니다.');
} else if (number % 3 == 1) {
print('나머지가 1입니다.');
} else {
// 위 조건 모두에 맞지 않기 때문에 다음 코드 실행
print('맞는 조건이 없습니다.');
}
}
enum과 함께 사용하면 유용하다.
enum Status {
approved,
pending,
rejected,
}
void main() {
Status status = Status.approved;
switch (status) {
case Status.approved:
// approved 값이기 때문에 다음 코드가 실행됩니다.
print('승인 상태입니다.');
break;
case Status.pending:
print('대기 상태입니다.');
break;
case Status.rejected:
print('거절 상태입니다.');
break;
default:
print('알 수 없는 상태입니다.');
}
}
void main() {
for (int i = 0; i < 3; i++) {
print(i);
}
List<int> numberList = [3, 6, 9];
for (int number in numberList) {
print(number);
}
}
void main() {
int total = 11;
// while문은 조건을 먼저 확인한 후 true가 반환되면 반복문을 실행하지만
while (total < 10) {
total += 1;
}
print(total); // 11
// do while은 반복문을 실행한 후 조건을 확인한다.
total = 11;
do {
total += 1;
} while(total < 10);
print(total); // 12
}
순서가 고정된 매개변수positional parameter(포지셔널 파라미터, 위치 매개변수)
입력된 순서대로 매개변수에 값이 지정된다.
int addTwoNumbers(int a, int b) {
return a + b;
}
void main() {
print(addTwoNumbers(1, 2)); // 3
}
[]
기호로 기본값을 설정할 수 있다.
// 두 번째 매개변수에 기본값 2를 적용
int addTwoNumbers(int a, [int b = 2]) {
return a + b;
}
void main() {
print(addTwoNumbers(1)); // 3
}
이름이 있는 매개변수named parameter(네임드 파라미터, 명명된 매개변수)
순서와 관계없이 지정하고 싶은 매개변수의 이름을 이용해 값을 입력한다.
중괄호{}
와 required
키워드를 사용
int addTwoNumbers({
required int a,
required int b,
}) {
return a + b;
}
void main() {
print(addTwoNumbers(a: 1, b: 2)); // 3
}
required
키워드를 생략하고 등호 다음에 기본값을 입력할 수 있다.
int addTwoNumbers({
required int a,
int b = 2,
}) {
return a + b;
}
void main() {
print(addTwoNumbers(a: 1)); // 3
}
포지셔널 파라미터와 네임드 파라미터를 섞어 사용할 때는 포지셔널 파라미터가 네임드 파라미터보다 반드시 먼저 위치해야 한다.
int addThreeNumbers(
int a, {
required int b,
int c = 2,
}) {
return a + b + c;
}
void main() {
print(addThreeNumbers(1, b: 3)); // 6
}
이름이 없고 일회성으로 사용된다.
익명 함수
(매개 변수) {
함수 바디
}
람다 함수
(매개변수) => 단 하나의 스테이트먼트
map()
, reduce()
, fold()
함수 등에서 일회성이 높은 로직을 작성할 때 주로 사용한다.typedef Operation = void Function(int x, int y);
void add(int x, int y) {
print('결괏값: ${x + y}');
}
void subtract(int x, int y) {
print('결괏값: ${x - y}');
}
void main() {
// typedef는 일반적인 변수의 type처럼 사용 가능
Operation oper = add;
oper(1, 2); // 결괏값: 3
// subtract() 함수도 Operation에 해당되는 시그니처이므로 oper 변수에 저장 가능
oper = subtract;
oper(1, 2); // 결괏값: -1
}
다트에서 함수는 일급 객체first-class citizen이므로 함수를 값처럼 사용할 수 있다. 따라서 다음과 같이 매개변수로 넣어 사용할 수 있다.
void calculate(int x, int y, Operation oper) {
oper(x, y);
}
void main() {
calculate(1, 2, add); // 결괏값: 3
}
void main() {
try {
final String name = '코드팩토리';
throw Exception('이름이 잘못됐습니다!'); // 고의로 에러 발생
print(name); // 실행되지 않음
} catch(e) {
print(e);
}
}
class Idol {
// 생성자에서 입력받을 변수는 일반적으로 final로 선언하는데 실수로 변경하는 것을 막기 위함이다.
final String name;
// 생성자는 클래스와 같은 이름이어야 한다.
Idol(String name) : this.name = name;
// 클래스에 종속되는 함수를 메서드라고 부른다.
void sayName() {
// 클래스 내부의 속성을 지칭하고 싶을 때는 this 키워드를 사용한다.
print('저는 ${this.name}입니다.');
// 스코프 안에 같은 속성 이름이 하나만 존재한다면 this를 생략할 수 있다.
// print('저는 ${name}입니다.');
}
}
void main() {
Idol blackPink = Idol('블랙핑크');
blackPink.sayName(); // 저는 블랙핑크입니다.
Idol BTS = Idol('BTS');
BTS.sayName(); // 저는 BTS입니다.
}
생성자의 매개변수를 변수에 저장하는 과정을 생략하는 방법도 있다.
class Idol {
...
// this를 사용할 경우
// 해당되는 변수에 자동으로 매개변수가 저장된다.
Idol(this.name);
...
}
일반적으로 클래스를 생성하는 여러 방법을 명시하고 싶을 때 사용한다.
class Idol {
final String name;
final int membersCount;
// 생성자
Idol(String name, int membersCount)
: this.name = name,
this.membersCount = membersCount;
// 네임드 생성자
// {클래스명.네임드 생성자명} 형식
Idol.fromMap(Map<String, dynamic> map)
: this.name = map['name'],
this.membersCount = map['membersCount'];
void sayName() {
print('저는 ${this.name}입니다. ${this.name} 멤버는 ${this.membersCount}명입니다.');
}
}
void main() {
Idol blackPink = Idol('블랙핑크', 4);
blackPink.sayName(); // 저는 블랙핑크입니다. 블랙핑크 멤버는 4명입니다.
Idol bts = Idol.fromMap({
'name': 'BTS',
'membersCount': 7,
});
bts.sayName(); // 저는 BTS입니다. BTS 멤버는 7명입니다.
}
일반적으로 프라이빗 변수는 클래스 내부에서만 사용하는 변수를 칭하지만 다트 언어에서는 같은 파일에서만 접근 가능한 변수입니다.
class Idol {
// '_'로 변수명을 시작하면 프라이빗 변수이다.
String _name;
Idol(this._name);
}
()
없이 사용한다. ex) blackPink.name
extends
키워드를 사용한다.class Idol {
final String name;
final int membersCount;
Idol(this.name, this.membersCount);
void sayName() {
print('저는 ${this.name}입니다.');
}
void sayMembersCount() {
print('${this.name} 멤버는 ${this.membersCount}명입니다.');
}
}
class BoyGroup extends Idol {
BoyGroup(
String name,
int membersCount,
) : super( // super는 상속한 부모 클래스를 지칭한다. 자식클래스 생성자에서 부모 생성자를 실행해줘야 한다.
name,
membersCount,
);
// 상속받지 않은 메서드는 변수를 새로 추가할 수 있다.
void sayMale() {
print('저는 남자 아이돌입니다.');
}
}
void main() {
BoyGroup bts = BoyGroup('BTS', 7);
bts.sayName(); // 저는 BTS입니다.
bts.sayMembersCount(); // BTS 멤버는 7명입니다.
bts.sayMale(); // 저는 남자 아이돌입니다.
}
class GirlGroup extends Idol {
GirlGroup(
super.name,
super.membersCount,);
@override // 생략해도 메서드가 덮어써진다. 하지만 직접 명시하는 게 협업 및 유지보수에 유리하다.
void sayName() {
print('저는 여자 아이돌 ${this.name}입니다.');
}
}
void main() {
GirlGroup blackPink = GirlGroup('블랙핑크', 4);
blackPink.sayName(); // 저는 여자 아이돌 블랙핑크입니다.
blackPink.sayMembersCount(); // 블랙핑크 멤버는 4명입니다.
}
implements
키워드로 원하는 클래스를 인터페이스로 사용할 수 있다.class GirlGroup implements Idol {
final String name;
final int membersCount;
GirlGroup(
this.name,
this.membersCount,
);
void sayName() {
print('저는 여자 아이돌 ${this.name}입니다.');
}
void sayMembersCount() {
print('${this.name} 멤버는 ${this.membersCount}명입니다.');
}
}
void main() {
GirlGroup blackPink = GirlGroup('블랙핑크', 4);
// 사용법은 클래스와 같다.
blackPink.sayName(); // 저는 여자 아이돌 블랙핑크입니다.
blackPink.sayMembersCount(); // 블랙핑크 멤버는 4명입니다.
}
with
키워드를 사용한다.mixin IdolSingMixin on Idol {
void sing() {
print('${this.name}이 노래를 부릅니다.');
}
}
class BoyGroup extends Idol with IdolSingMixin {
BoyGroup(
super.name,
super.membersCount,
);
void sayMale() {
print('저는 남자 아이돌입니다.');
}
}
void main() {
BoyGroup bts = BoyGroup('BTS', 7);
bts.sing(); // BTS이 노래를 부릅니다.
}
abstract class Idol {
final String name;
final int membersCount;
Idol(this.name, this.membersCount);
// 추상 메서드 선언
void sayName();
void sayMembersCount();
}
class GirlGroup implements Idol {
final String name;
final int membersCount;
GirlGroup(
this.name,
this.membersCount,
);
void sayName() {
print('저는 여자 아이돌 ${this.name}입니다.');
}
void sayMembersCount() {
print('${this.name} 멤버는 ${this.membersCount}명입니다.');
}
}
void main() {
GirlGroup blackPink = GirlGroup('블랙핑크', 4);
blackPink.sayName(); // 저는 여자 아이돌 블랙핑크입니다.
blackPink.sayMembersCount(); // 블랙핑크 멤버는 4명입니다.
}
// 인스턴스화할 때 입력받을 타입을 T로 지정
class Cache<T> {
// data의 탕비을 추후 입력될 T 타입으로 지정
final T data;
Cache({
required this.data,
});
}
void main() {
// T의 타입을 List<int>로 입력
final cache = Cache<List<int>>(
data: [1, 2, 3],
);
// 제네릭에 입력된 값을 통해 data 변수의 타입 자동 유추
print(cache.data.reduce((value, element) => value + element)); // 6
}
문자 | 설명 | 예시 |
---|---|---|
T | 변수 타입을 표현할 때 | T value; |
E | 리스트 내부 요소들의 타입을 표현할 때 | List<E> |
K | 키를 표현할 때 | Map<K, V> |
V | 값을 표현할 때 | Map<K, V> |
static 키워드를 사용하면 변수와 메서드 등 모든 속성은 '인스턴스'가 아닌 클래스 자체에 귀속된다.
class Counter {
static int i = 0;
// static 변수는 클래스에 직접 귀속되기 때문에 생성자에서 값을 지정하지 못함
Counter() {
i++;
print(i);
}
}
void main() {
Counter count1 = Counter(); // 1
Counter count2 = Counter(); // 2
Counter count3 = Counter(); // 3
}
..
기호를 사용class Idol {
final String name;
final int membersCount;
Idol(this.name, this.membersCount);
void sayName() {
print('저는 ${this.name}입니다.');
}
void sayMembersCount() {
print('${this.name} 멤버는 ${this.membersCount}명입니다.');
}
}
void main() {
Idol blackPink = Idol('블랙핑크', 4)
..sayName() // 저는 블랙핑크입니다.
..sayMembersCount(); // 블랙핑크 멤버는 4명입니다.
}
비동기 프로그래밍은 요청한 결과를 기다리지 않으며 응답 순서 또한 요청한 순서와 다를 수 있다.
Future<String> name; // 미래에 받을 String 값
Future<int> number; // 미래에 받을 int 값
Future<bool> isOpened; // 미래에 받을 boolean 값
void main() {
addNumbers(1, 1);
}
void addNumbers(int number1, int number2) {
print('$number1 + $number2 계산 시작!');
// 특정 기간 동안 아무것도 하지 않고 기다리는 함수
// 첫번째 매개변수 - 대기할 기간, 두번째 매개변수 - 대기 후 실행할 콜백 함수
Future.delayed(Duration(seconds: 3), () {
print('$number1 + $number2 = ${number1 + number2}');
});
print('$number1 + $number2 코드 실행 끝');
}
// 1 + 1 계산 시작!
// 1 + 1 코드 실행 끝
// 1 + 1 = 2
void main() {
addNumbers(1, 1);
}
Future<void> addNumbers(int number1, int number2) async {
print('$number1 + $number2 계산 시작!');
// await는 대기하고 싶은 비동기 함수 앞에 입력
await Future.delayed(Duration(seconds: 3), () {
print('$number1 + $number2 = ${number1 + number2}');
});
print('$number1 + $number2 코드 실행 끝');
}
1 + 1 계산 시작!
1 + 1 = 2
1 + 1 코드 실행 끝
void main() {
addNumbers(1, 1);
addNumbers(2, 2); // addNumbers() 함수가 비동기로 실행되었기 때문에 1 + 1이 끝나기 전에 실행
}
1 + 1 계산 시작!
2 + 2 계산 시작!
1 + 1 = 2
1 + 1 코드 실행 끝
2 + 2 = 4
2 + 2 코드 실행 끝
// addNumbers()가 순차적으로 실행되길 원한다면 아래와 같이 async, await 키워드를 추가
void main() async{
await addNumbers(1, 1);
await addNumbers(1, 2);
}
1 + 1 계산 시작!
1 + 1 = 2
1 + 1 코드 실행 끝
2 + 2 계산 시작!
2 + 2 = 4
2 + 2 코드 실행 끝
void main() async{
final result = await addNumbers(1, 1);
print('결괏값 $result');
final result2 = await addNumbers(2, 2);
print('결괏값 $result2');
}
Future<int> addNumbers(int number1, int number2) async {
print('$number1 + $number2 계산 시작!');
await Future.delayed(Duration(seconds: 3), () {
print('$number1 + $number2 = ${number1 + number2}');
});
print('$number1 + $number2 코드 실행 끝');
return number1 + number2;
}
1 + 1 계산 시작!
1 + 1 = 2
1 + 1 코드 실행 끝
결괏값 2
2 + 2 계산 시작!
2 + 2 = 4
2 + 2 코드 실행 끝
결괏값 4
import 'dart:async';
void main() {
final controller = StreamController();
final stream = controller.stream;
// Stream에 listen() 함수를 실행하면 값이 주입될 때마다 콜백 함수를 실행한다.
final streamListener1 = stream.listen((val) {
print(val);
});
// Stream에 값을 주입하기
controller.sink.add(1);
controller.sink.add(2);
controller.sink.add(3);
controller.sink.add(4);
}
1
2
3
4
스트림을 여러 번 listen()하도록 변환할 수 있다.
import 'dart:async';
void main() {
final controller = StreamController();
// 여러 번 리슨할 수 있는 Broadcast Stream 객체 생성
final stream = controller.stream.asBroadcastStream();
// 첫 번째 listen() 함수
final streamListener1 = stream.listen((val) {
print('listening 1');
print(val);
});
// 두 번째 listen() 함수
final streamListener2 = stream.listen((val) {
print('listening 2');
print(val);
});
// add()를 실행할 때마다 listen()하는 모든 콜백 함수에 값이 주입된다.
controller.sink.add(1);
controller.sink.add(2);
controller.sink.add(3);
}
listening 1
1
listening 2
1
listening 1
2
listening 2
2
listening 1
3
listening 2
3
Future
를 반환하는 함수는 async
로 함수를 선언하고 return
키워드로 값을 반환async*
로 함수를 선언하고 yield
키워드로 값을 반환import 'dart:async';
Stream<String> calculate(int number) async* {
for (int i = 0; i<5; i++) {
yield 'i = $i';
await Future.delayed(Duration(seconds: 1));
}
}
void playStream() {
calculate(1).listen((val) {
print(val);
});
}
void main() {
playStream();
}
i = 0
i = 1
i = 2
i = 3
i = 4