var author = "개발하는남자"
String stringValue = "문자열";
int intValue = 2;
bool booleanValue = false;
변수란?
어떠한 값을 담아두고 필요할때 꺼내쓰는 박스
타입이란?
변수(박스)에 담길 값들의 종류


score, tryCount 변수를 int로 선언하고 Text위젯에 score 값(정수)을 toString으로 형변환해서 넣는다.

원하는 100의 점수가 나오는 모습
추가적으로 tryCount와 cards 리스트도 수정해주었다.


반복문이란?
반복해서 무엇인가를 처리할 때 사용하는 기술

반복시 시작될때 단 한번 실행이 되며 주로 초기화 하는데 사용
반복이 계속 진행되는지 결정하는 조건식
참이면 실행, 거짓이면 종료 ( ~ 까지 )
각 반복이 끝날때마다 실행되며 변수를 증감하거나 어떤 값을 업데이트하는데 사용
반복문이 실행되는동안 실행할 코드
반복문에는 for 문 외에도

카드의 숫자를 랜덤한 숫자로 넣고

반복되는 CardWidget(),코드를 for 반복문으로 수정

cardNumber는 기본이 1로 되어있어서 원하는 값 cards[i]로 넣어주고 isFlipped는 게임진행을 위해 제거
함수란?
프로그램 내에서 특정 작엄을 수행하기위해 비지니스 로직을 작성하여 원하는 값을 반환하거나 데이터를 담아두는 기능을 하는 것.

함수가 반환할 값의 타입을 명시 없을경우 void
함수를 호출할떄 사용하는 이름
함수가 입력받는 값들. 콤마로 여러개 정의 가능
함수가 수행할 작업
함수가 작업한뒤 호출한곳으로 돌려주는값 return 키워드 사용, 반환타입과 같은 타입으로 반환해야함.
두수의 덧셈 예제
int add(int a, int b) {
int sum = a + b;
return sum;
}
void main() {
int result = add(3, 5);
print('결과: $result'); // 출력: 결과: 8
}
int substract(int a, int b) {
int sum = a - b;
return sum;
}
void main() {
int result = substract(3, 5);
int result2 = substract(5, 3);
print('결과: $result'); // 출력: 결과: -2
print('결과: $result2'); // 출력: 결과: 2
}
substarct함수 매개변수로 a와 b가 들어가지만 a+b b+a 처럼 순서가 상관없는 덧셈 함수에 비해
뺄셈 함수는 a와 b의 순서가 중요함.
이처럼 파라미터 순서를 중요하게 받아 처리하는 방식을 포지셔널 파라미터라 부른다.
int substract({required int frontValue,required int endValue}) {
int sum = frontValue - endValue;
return sum;
}
void main() {
int result = substract(frontValue : 3 , endValue : 5);
int result2 = substract(endValue : 5 , frontValue : 3);
print('결과: $result'); // 출력: 결과: -2
print('결과: $result2'); // 출력: 결과: -2
}
포지셔널 파라미터에 비해 직관적인게 장점이다. 또한 옵셔널한 파라미터가 가능하다.
옵셔널한 파라미터? (optional parameters)
말그대로 옵셔널하게 넣을 수도 안 넣을 수도 있는 파라미터이다.
int add({required int frontValue,required int endValue, int? expansionValue}) {
int sum = frontValue + endValue + (expansionValue??0);
return sum;
}
void main() {
int result = add(frontValue : 3, endValue : 5);
int result2 = add(frontValue : 3, endValue : 5, expansionValue : 3);
print('결과: $result'); // 출력: 결과: 8
print('결과: $result2'); // 출력: 결과: 11
}
같은 add 함수 이지만 expansionValue는 넣을 수도 안 넣을 수도 있는 파라미터이다.
이때 안넣었을때 없을 값을 대비해 ?(nullable) 타입으로 지정한다.

onTapCard 함수를 생성해 클릭한 인덱스를 출력하게 만들고 CardWidget에 onTap 호출시 실행이 되게 코드를 수정

뒤집어 졌는지 상태를 저장하기위한 배열 선언

각 카드 위젯에 상태 정보를 넣어준다.

클릭시마다 뒤집어지게 값을 넣어주고 실제 화면에서 반영이 되게 하기위해 StatelessWidget인 CardBoards를 StateFulWidget으로 수정


그러면 이렇게 StatefulWidget으로 바뀌고 기존에 있던 코드들은 CardBoards의 State로 넘어가게된다.

이제 값을 바꾼후 화면을 리빌드하기위한 setState까지 넣어주면 
이렇게 클릭시 카드가 뒤집어지는 화면을 볼 수 있게된다.
조건문이란?
특정 조건을 만족할때만 특정 코드가 실행하도록 하는 제어문

예시
void main() {
int number = 3;
if (number > 5) {
print('number는 5보다 큽니다.');
} else {
print('number는 5보다 크지 않습니다.');
}
}

예시
void main() {
int number = 2;
switch (number) {
case 1:
print('number는 1입니다.');
break;
case 2:
print('number는 2입니다.');
break;
case 3:
print('number는 3입니다.');
break;
default:
print('number는 1, 2, 3 중 하나가 아닙니다.');
}
}
가독성을 높이거나 특정 값으로 비교할때, enum 타입같은 열거형으로 비교할때.
먼저고른 카드와 나중에 고른 카드를 비교하는 코드작성.
int instantFirstCard = -1;
먼저 고른 카드를 저장할 변수 선언
-1일경우 첫번째 카드를 고른 상태
-1이 아닐경우는 두번째카드를 고른상태이기 때문에 if/else로 분기 처리해준다.
if (instantFirstCard == -1) {
//첫번째 카드를 클릭했을때 로직
instantFirstCard = cardIndex;
} else {
// 두번째 카드가 선택되었을때 로직
}
이후 첫번째로 고른카드와 두번째로 고른 카드가 같다면 둘다 오픈, 틀렸을땐 다시 닫히는 코드를 작성해본다
void onTapCard(int cardIndex) {
print('$cardIndex 번째 카드를 선택하셨습니다.');
if (instantFirstCard == -1) {
//첫번째 카드를 클릭했을때 로직
instantFirstCard = cardIndex;
} else {
// 두번째 카드가 선택되었을때 로직
var firstCard = cards[instantFirstCard];
var secondCard = cards[cardIndex];
if (firstCard == secondCard) {
print('짝이 맞았습니다.');
} else {
cardsFlippedState[instantFirstCard] = false; // 추가
cardsFlippedState[cardIndex] = false; // 추가
}
}
setState(() {
cardsFlippedState[cardIndex] = true;
});
}
여기서 발생하는 문제는 아래에 setstate이다. 우린 클릭시 화면이 열리는 코드를 작성했기때문에 짝이 맞지 않는다면 닫아주는 코드를 작성하고 로직을 종료해야한다 따라서 setstate안에 넣어주고 return을 붙여준다.
void onTapCard(int cardIndex) {
print('$cardIndex 번째 카드를 선택하셨습니다.');
if (instantFirstCard == -1) {
//첫번째 카드를 클릭했을때 로직
instantFirstCard = cardIndex;
} else {
// 두번째 카드가 선택되었을때 로직
var firstCard = cards[instantFirstCard];
var secondCard = cards[cardIndex];
if (firstCard == secondCard) {
print('짝이 맞았습니다.');
} else {
setState(() {
cardsFlippedState[instantFirstCard] = false; // 추가
cardsFlippedState[cardIndex] = false; // 추가
});
}
return;
}
setState(() {
cardsFlippedState[cardIndex] = true;
});
}
이때 생기는 문제로는 instantFirstCard 값을 수정해주지 않아 계속 한가지 카드로만 비교를 하게된다. -1로 초기화를 계속 진행해주자.
두번째 문제가 발생했다.
두개의 카드가 맞지않을때 카드를 뒤집지않고 진행이 되지만 사용자 입장에서는 두번째 카드가 무엇인지 확인을 못하는 내용이 발생한다.
개발하고자하는 방향은 두개의 카드화면을 확인 후 맞지않으면 2초후 다시 뒤집는 거기 때문에 이 2초에 관련한 내용은 다음 수업에 배우기로한다.
동기/비동기란?
동기

void main() {
print('작업 1 시작');
performTask();
print('작업 1 완료');
}
void performTask() {
print('작업 2 실행');
}
출력
작업 1 시작
작업 2 실행
작업 1 완료
비동기

void main() {
print('작업 1 시작');
performTask();
print('작업 1 완료');
}
Future<void> performTask() async {
await Future.delayed(Duration(seconds: 2));
print('작업 2 실행');
}
출력
작업 1 시작
작업 1 완료
(2초뒤에)
작업 2 실행
async 키워드
await 키워드
void main() async{
print('작업 1 시작');
await performTask();
print('작업 1 완료');
}
Future<void> performTask() async {
await Future.delayed(Duration(seconds: 2));
print('작업 2 실행');
}
출력
작업 1 시작
(2초뒤에)
작업 2 실행
작업 1 완료
void onTapCard(int cardIndex) {
print('$cardIndex 번째 카드를 선택하셨습니다.');
if (instantFirstCard == -1) {
//첫번째 카드를 클릭했을때 로직
instantFirstCard = cardIndex;
} else {
// 두번째 카드가 선택되었을때 로직
var firstCard = cards[instantFirstCard];
var secondCard = cards[cardIndex];
if (firstCard == secondCard) {
print('짝이 맞았습니다.');
instantFirstCard = -1; // 추가
} else {
// 함수분리
resetInstantCards(instantFirstCard, cardIndex);
}
}
setState(() {
cardsFlippedState[cardIndex] = true;
});
}
void resetInstantCards(int firstIndex, int secondIndex) {
setState(() {
cardsFlippedState[firstIndex] = false; // 추가
cardsFlippedState[secondIndex] = false; // 추가
});
instantFirstCard = -1; // 추가
return;
}
조건에 맞춰서 함수 분리를 진행한다. resetInstantCards();
또한 2초후 카드가 다시 뒤집어져야하기때문에 위에서 배운 async/await를 사용한다.
void resetInstantCards(int firstIndex, int secondIndex) async {
await Future.delayed(Duration(seconds: 2)); // 추가
setState(() {
cardsFlippedState[firstIndex] = false;
cardsFlippedState[secondIndex] = false;
});
instantFirstCard = -1;
}
플러터의 모든것은 위젯으로 되어 있기 때문에 tree구조를 가질 수 밖에없다.

이때 cardboards 위젯에서 발생한 일은 Header의 Text(tryCount)위젯에 직접 전달이 될수가 없기때문에 두곳의 상위인 home에서 진행을 하게 된다.
class Header extends StatelessWidget {
final int tryCount;
Header({super.key, this.tryCount = 0});
int score = 100;
Header 위젯에서 값을 받을수있도록 tryCount를 선언하고
class Home extends StatelessWidget {
int tryCount = 0; // 추가
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xffECE7E4),
appBar: AppBar(
title: const Text('짝맞추기 게임'),
backgroundColor: const Color(0xff92CBFF),
),
body: Padding(
padding: EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Header(tryCount: tryCount), // 추가
SizedBox(height: 20),
Expanded(child: CardBoards()),
],
),
),
);
}
}
home에서 Header위젯으로 값을 넘겨준다.
이제 tryCount를 증가시키기위해 카드가 눌렸다는 정보를 받아야 한다.
class CardBoards extends StatefulWidget {
final Function() updateTryCount;
CardBoards({super.key, required this.updateTryCount});
직접 클릭을 받는 CardBoards에 updateTryCount 함수를 넘겨준다.
void updateTryCount() {
print('시도 횟수를 업데이트합니다.');
}
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xffECE7E4),
appBar: AppBar(
title: const Text('짝맞추기 게임'),
backgroundColor: const Color(0xff92CBFF),
),
body: Padding(
padding: EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Header(tryCount: tryCount),
SizedBox(height: 20),
Expanded(
child: CardBoards(
updateTryCount: updateTryCount,
),
),
],
),
),
);
}
이제 CardBoards에서 건내받은 updateTryCount를 실행할수 있게 코드를 수정한다.
void onTapCard(int cardIndex) {
print('$cardIndex 번째 카드를 선택하셨습니다.');
if (instantFirstCard == -1) {
instantFirstCard = cardIndex;
} else {
// 두번째 카드가 선택되었을때 로직 추가
widget.updateTryCount(); // 추가
var firstCard = cards[instantFirstCard];
var secondCard = cards[cardIndex];
if (firstCard == secondCard) {
print('짝이 맞았습니다.');
instantFirstCard = -1;
} else {
resetInstantCards(instantFirstCard, cardIndex);
}
}
setState(() {
cardsFlippedState[cardIndex] = true;
});
}
widget.updateTryCount를 호출하는 이유는 전달받는곳은 CardBoards 이곳이지만. 이 위젯의 State로 갖고있는 _CardBoardsState에서 실제 동작이 이루어 지며 CardBoards의 updateTryCount를 widget. 으로 접근이 가능하다.
이후 Home위젯에서 updateTryCount가 호출될때 +1씩 업데이트가 되기위해 화면을 새로 빌드하는 setstate를 작성한다.
void updateTryCount() {
setState(() {
tryCount++;
});
}
클래스(객체)란?
객체는 고유한 속성과 기능을 갖고있는것.
class [클래스명] {
생성자()
맴버변수//특성
메서드(){
//기능
}
}
class Human{
final String id;
final String name;
final int age;
final Gender gender;
}
이때 Gender 타입은 Enum으로 작성됭 타입
enum Gender{
M,W
}
final이란?
한번 값을 주입하면 바꿀 수 없음을 compiler에게 알려주는 것
class Human{
final String id;
final String name;
final int age;
final Gender gender;
const Human( // 생성자
this.id,
this.name,
this.age,
this.gender,
);
}
void addAge(){
age++;
}
void main() async {
var totalYears = 10;
print('프로그램 시작');
var man = Human(
id: '961109-xxxxxxx',
name: '유일송',
age: 30,
gender: Gender.M,
);
for (var currentYear = 1; currentYear < totalYears; currentYear++) {
await Future.delayed(Duration(seconds: 1));
print('$currentYear년이 흘렀습니다.');
man.addAge();
print('${man.name}은 ${man.age} 나이입니다.');
}
print('프로그램 종료');
}
출력결과

이번엔 객체를 활용해서 Card 모델을 만들어볼까한다.
card의 특성은 총 세개
class CardModel {
final int index;
final int cardValue;
bool isFlipped;
CardModel({
required this.index,
required this.cardValue,
this.isFlipped = false, //기본적으로 뒤집어진상태
});
}
추가적으로 뒤집히는 함수까지 작성
void setFlipped(bool state) { // 추가
isFlipped = state;
}
강제로 넣었던 cardsValue를 shuffle 함수로 섞고 List.generate로 모델에 값을넣어 모델리스트를 생성해준다.
추가로 첫번째 카드인지 체크하던 instantFirstCard 값 역시 CardModel로 바꿔준다
class _CardBoardsState extends State<CardBoards> {
late List<CardModel> cards;
CardModel? instantFirstCard;
void initState() {
super.initState();
List<int> cardsValue = [1, 5, 2, 6, 3, 4, 3, 2, 6, 1, 4, 5];
cardsValue.shuffle();
cards = List.generate(cardsValue.length, (index) {
return CardModel(index: index, cardValue: cardsValue[index]);
});
}
CardWidget이 더이상 int cardNumber로 접근을 하지 않기때문에 이 위젯 역시 수정을 해준다.
첫번째는 파라미터 이름
CardWidget(
card: cards[i],
onTap: () {
onTapCard(i);
},
),
그외 cardModel에 접근하는 값 (cardValue, isFlipped)도 수정을 해준다.
class CardWidget extends StatelessWidget {
final CardModel card;
final Function()? onTap;
const CardWidget({
super.key,
required this.card,
this.onTap,
});
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (onTap != null && !card.isFlipped) {
// 수정
onTap!();
}
},
child: Container(
width: 115,
height: 150,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
offset: Offset(0, 2),
blurRadius: 4,
),
],
),
child: card.isFlipped // 수정
? Center(
child: Image.asset('assets/images/${card.cardValue}.png'), // 수정
)
: Container(
margin: EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Color(0xffBBD0E3),
),
child: Center(child: Image.asset('assets/images/logo.png'))),
),
);
}
}
마지막으로 CardBoards에서 CardModel? instantFirestCard 를 사용하게되는 부분에 대한 null관련 코드 까지수정
void onTapCard(int cardIndex) {
print('$cardIndex 번째 카드를 선택하셨습니다.');
if (instantFirstCard == null) {
instantFirstCard = cards[cardIndex];
} else {
// 두번째 카드가 선택되었을때 로직 추가
widget.updateTryCount();
var firstCard = instantFirstCard;
var secondCard = cards[cardIndex];
if (firstCard!.cardValue == secondCard.cardValue) {
print('짝이 맞았습니다.');
instantFirstCard = null;
} else {
resetInstantCards(instantFirstCard!, secondCard);
}
}
setState(() {
cards[cardIndex].setFlipped(true);
});
}
void resetInstantCards(CardModel firstCard, CardModel secondCard) async {
await Future.delayed(Duration(seconds: 2));
setState(() {
firstCard.setFlipped(false);
secondCard.setFlipped(false);
});
instantFirstCard = null;
return;
}
마지막 정리 파트에 숙제라고 적힌 내용이 있는데

본 강의가 아닌 사전으로 배포한 강의다 보니 뒤에 숙제관련 두강의는 빠졋나보다...

그래도 적힌대로 진행을 해본다.
첫번째 과제는 짝을 맞추면 score값을 100씩 올리기.
기존에 짜둔 updateTryCount와 tryCount를 활용하면 쉽게 짤수있다
int tryCount = 0;
int score = 0;
...
Header(tryCount: tryCount, score: score),
SizedBox(height: 20),
Expanded(
child: CardBoards(
updateTryCount:updateTryCount,
updateScore: updateScore,
)),
...
void updateScore() {
setState(() {
score += 100;
});
}
스코어 값을 변수로 두고
Header에는 두개의 값, 그리고 CardBoards에 스코어값을 증가하는 함수를 넣어준다.
Header에서는 선언했었던 값만 파라미터로 넘겨주는 값으로 치환 해주면된다
const Header({super.key, this.tryCount = 0, this.score = 0});
final int score;
final int tryCount;
CardBoards 역시 updateTryCount 사용했을때를 참고하여 진행한다
final Function() updateTryCount;
final Function() updateScore;
const CardBoards({super.key, required this.updateTryCount, required this.updateScore});
그리고 짝이 맞았을때의 위치에 widget.updateCount를 작성하면 첫번째 과제는 끝!
void onTapCard(int cardIndex) {
print('$cardIndex 번째 카드를 선택하셨습니다.');
if (instantFirstCard == null) {
instantFirstCard = cards[cardIndex];
} else {
// 두번째 카드가 선택되었을때 로직 추가
widget.updateTryCount();
var firstCard = instantFirstCard;
var secondCard = cards[cardIndex];
if (firstCard!.cardValue == secondCard.cardValue) {
print('짝이 맞았습니다.');
instantFirstCard = null;
widget.updateScore(); // Count 증가!
} else {
resetInstantCards(instantFirstCard!, secondCard);
}
}
setState(() {
cards[cardIndex].setFlipped(true);
});
}
다음 과제는 새게임을 눌렀을때 scrore, tryCount가 0이되고 모든 카드가 뒤집어지며 초기화까지 해야한다.
우선 scrore, tryCount 부터!
리셋 시키는 함수를 만들어 보내준다
Header(tryCount: tryCount, score: score, resetGame: resetGame),
...
void resetGame() {
setState(() {
score = 0;
tryCount = 0;
});
}
함수를 매개변수로 받은 Header쪽에선
받을 변수를 하나 만들어두고 GestureDetector를 이용해 ontap일때 실행을 해준다.
const Header({super.key, this.tryCount = 0, this.score = 0, this.resetGame});
final int score;
final int tryCount;
final Function()? resetGame;
...
Expanded(
child: GestureDetector(
child: Container(
margin: EdgeInsets.only(top: 10, bottom: 10, left: 20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
color: Color(0xff94BEE5),
),
child: Center(child: Text('새 게임')),
),
onTap: () {
resetGame!();
},
)),
그 다음인 카드 뒤집기와 다시섞기는 조금 복잡하다. cardboards와 Header는 Home에서 연결이 되기때문에 카드 관련 정보도 Home으로 끄집어올린다.
카드 정보가 있는 리스트를 Home에서 선언을 하고 카드들이 리셋되는 setCard() 함수를 initstate에서 호출을 하게 한다(시작할때도 동일한 작동을 하기때문에)
이후 setCard() 함수를 선언하고 그안에서 카드 세팅에 대한 코드를 작성하고 header에 setcard를 호출하도록 넘겨준다.
정해진 카드는 CardBoards에서 사용할수있게 cards 이름으로 넘겨준다
late List<CardModel> cards;
void initState() {
super.initState();
setCard();
}
...
void setCard() {
List<int> cardsValue = [1, 5, 2, 6, 3, 4, 3, 2, 6, 1, 4, 5];
cardsValue.shuffle();
cards = List.generate(cardsValue.length, (index) {
return CardModel(index: index, cardValue: cardsValue[index]);
});
}
...
Header(tryCount: tryCount, score: score, resetGame: resetGame, setCard: setCard),
SizedBox(height: 20),
Expanded(
child: CardBoards(
cards: cards,
updateTryCount: updateTryCount,
updateScore: updateScore,
)),
Header에서는 이전 함수들과 같이 넘겨받은 setCard를 사용해준다
const Header(
{super.key,
this.tryCount = 0,
this.score = 0,
this.resetGame,
this.setCard});
final int score;
final int tryCount;
final Function()? resetGame;
final Function()? setCard;
...
onTap: () {
resetGame!();
setCard!();
},
마지막으로 카드정보를 모두 넘겨준 CardBoardsState를 수정한다.
class _CardBoardsState extends State<CardBoards> {
CardModel? instantFirstCard;
// late List<CardModel> cards;
// @override
// void initState() {
// super.initState();
// List<int> cardsValue = [1, 5, 2, 6, 3, 4, 3, 2, 6, 1, 4, 5];
// cardsValue.shuffle();
// cards = List.generate(cardsValue.length, (index) {
// return CardModel(index: index, cardValue: cardsValue[index]);
// });
// }
넘겨받는 cards를 선언해주고 불필요한 셋팅 내용들은 모두 주석처리를 한다.
final Function() updateTryCount;
final Function() updateScore;
final List<CardModel> cards;
const CardBoards(
{super.key, required this.updateTryCount, required this.updateScore, required this.cards});
...
class _CardBoardsState extends State<CardBoards> {
CardModel? instantFirstCard;
// late List<CardModel> cards;
// @override
// void initState() {
// super.initState();
// List<int> cardsValue = [1, 5, 2, 6, 3, 4, 3, 2, 6, 1, 4, 5];
// cardsValue.shuffle();
// cards = List.generate(cardsValue.length, (index) {
// return CardModel(index: index, cardValue: cardsValue[index]);
// });
// }
cards로 사용되던 모든 값들을 widget.cards로 수정을하면 끝이다
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Wrap(
spacing: 4,
runSpacing: 4,
children: [
for (var i = 0; i < widget.cards.length; i++)
CardWidget(
card: widget.cards[i],
onTap: () {
onTapCard(i);
},
),
],
),
);
}
void onTapCard(int cardIndex) {
print('$cardIndex 번째 카드를 선택하셨습니다.');
if (instantFirstCard == null) {
instantFirstCard = widget.cards[cardIndex];
} else {
// 두번째 카드가 선택되었을때 로직 추가
widget.updateTryCount();
var firstCard = instantFirstCard;
var secondCard = widget.cards[cardIndex];
if (firstCard!.cardValue == secondCard.cardValue) {
print('짝이 맞았습니다.');
instantFirstCard = null;
widget.updateScore(); // Count 증가!
} else {
resetInstantCards(instantFirstCard!, secondCard);
}
}
setState(() {
widget.cards[cardIndex].setFlipped(true);
});
}
