개발/클론코딩
[Nomad Coders] Flutter로 Pomodoros 앱 만들기
걍판자
2024. 1. 10. 23:35
반응형
https://nomadcoders.co/flutter-for-beginners
이 글은 Nomad Coders 님의 강의를 참고해 작성하였습니다.
작성 날짜 기준 무료 강의로, 누구나 쉽게 가입하고 배울 수 있습니다.
2024.01.10 - [개발] - [Nomad Coders] Flutter로 UI 만들기
Stateful Widgets
state
import 'package:flutter/material.dart';
void main() {
runApp(App());
}
class App extends StatefulWidget {
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
int counter = 0;
void onClicked() {
counter = counter + 1;
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: const Color(0xFFF4EDDB),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Click Count',
style: TextStyle(fontSize: 30),
),
Text(
'$counter',
style: const TextStyle(fontSize: 30),
),
IconButton(
iconSize: 40,
onPressed: onClicked,//버튼이 눌렸을때 OnClicked 함수 발동
icon: const Icon(
Icons.add_box_rounded,
),
),
],
),
),
),
);
}
}
- 지금까지 사용한 stateless widget은 그냥 build 메서드를 통해 정적인 내용을 출력한다.
- stateful widget은 이와 달리 동적으로 상태에 따라 변한다.
- statelss는 데이터가 없다는 뜻이다. stateful은 데이터 즉 상태가 있는 것이다.
- stateful widget을 사용하면 실시간 데이터 변화를 ui를 통해 볼 수 있다.
- stateful widget은 다음과 같이 계승받는 부분과 하는 부분 2가지로 나뉜다
- 다음 코드에서는 버튼을 눌러도 숫자가 증가하지 않는다 왜 그럴까?
setState
void onClicked() {
setState(){
counter = counter + 1;
}
}
void onClicked() {
setState(){}
counter = counter + 1;
}
//setState의 중괄호 위치 상관 없이 둘다 된다.
- 변경된 값을 보기 위해서는 setState(){} 안에서 변경된 값을 호출해주어야 한다. setstate를 통해 build 메서드를 다시 호출한다. setState를 통해 다시 만드는 것이다.
- 꼭 수정되는 데이터 코드들을 setState 안에 넣을 필요는 없다. setState를 호출하는 것 만으로 그냥 업데이트가 된다. 하지만 가능하면 가독성을 위해 setState 안에 넣도록 하자
- state를 사용하지 않는다고 데이터 자체가 불변하는 것은 아니다. 다만 변경된 데이터가 화면에 표시되지 않는 것이다.
- 즉 필요한 건 stateful 했을때 나오는 것에서 다 아래쪽에 적고 변경필요한 건 setState 안에 적으면 된다.
- setState는 위젯의 새로고침과 같은 기능을 준다.
BuildContext
import 'package:flutter/material.dart';
void main() {
runApp(App());
}
class App extends StatefulWidget {
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
textTheme: const TextTheme(
titleLarge: TextStyle(
color: Colors.red,
),
),
),
home: Scaffold(
backgroundColor: const Color(0xFFF4EDDB),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
MyLargeTitle(),
],
),
),
),
);
}
}
class MyLargeTitle extends StatelessWidget {
const MyLargeTitle({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(
'My Large Title',
style: TextStyle(
fontSize: 30,
color: Theme.of(context).textTheme.titleLarge?.color,
),
);
}
}
- Context는 위젯의 상하관계에서 더 상위관계의 위젯 그러니까 더 일찍 호출되는 더 뿌리가 되는 위젯의 성질을 계승할 수 있게 한다.
- 이는 class를 위젯으로 따로 분리시켜도 마찬가지로 작동한다.
- context를 계승할때 null이 아니어도 dart는 null일수도 있다고 생각해 오류창을 띄울 수 있다. 이때는 ?를 통해 null이 아닐 때만 처리한다고 하거나, 이건 null이 아니라고 확신을 주는! 를 쓰면 된다.
- theme: ThemeData를 통해 기존의 테마 값들을 저장해놓을 수 있다.
- Theme.of(context)를 통해 상위 값들에 접근 할 수 있다.
- 상위 계층 중 가장 가까운 값을 받는다고 한다.
Widget LifeCycle
void initState(){
super.iniState();
}
void dispose(){
super.dispose();
}
- initState()는 build를 하기 전 불러올때 실행된다. 대표적으로 API를 불러올 때 사용된다
- dispose()는 화면에서 사라질 때 실행한다.
- 꼭 initState()를 쓰지 않아도 build override 전에 변숫값을 초기화하거나 하는 식으로 초기상황을 불러올 수 있다. 부모요소에 의존하는 데이터를 초기화하기 위해 주로 쓰게 된다.
Pomodoro App
UI
import 'package:flutter/material.dart';
void main() {
runApp(App());
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData( // theme 데이터 다른곳에도 불러오기 위한 빌드업
backgroundColor: const Color(0xFFE7626C),//붉은 배경
textTheme: const TextTheme(
headline1: TextStyle(
color: Color(0xFF232B55),//검은 글자
),
),
cardColor: const Color(0xFFF4EDDB),//흰 카드
),
home: const HomeScreen(),//HomeScreen class 불러오기
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState(); //Stateful이어서 있는 것
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,//theme에서 불러오기
body: Column(
children: [ // column의 children으로 flexible이 셋, 각자 화면 비율을 1 :3:1 로 가져간다.
Flexible(
flex: 1,
child: Container( //container 생성 후 테마 받아 타이머 글자 ui 표시
alignment: Alignment.bottomCenter,// row의 아래중앙 정렬
child: Text(
'25:00',
style: TextStyle(
color: Theme.of(context).cardColor,
fontSize: 89,
fontWeight: FontWeight.w600,
),
),
),
),
Flexible(
flex: 3,
child: Center( //중앙정렬
child: IconButton(
iconSize: 120,
color: Theme.of(context).cardColor,
onPressed: () {},//아이콘버튼이니 눌렀을때 반응 함수 필요
icon: const Icon(Icons.play_circle_outline),
),
),
),
Flexible(
flex: 1,
child: Row( //컨테이너가 더 넓게 열 전체를 차지하게 하려고 row로 지정
children: [
Expanded( // 그후 expanded로 row의 모든 공간 차지하게 함
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,//좌우 중앙 정렬
children: [
Text(
'Pomodoros',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Theme.of(context).textTheme.headline1!.color,
),
),
Text(
'0',
style: TextStyle(
fontSize: 58,
fontWeight: FontWeight.w600,
color: Theme.of(context).textTheme.headline1!.color,
),
),
],
),
),
),
],
),
)
],
),
);//return scaffold
}
}
- flex는 해당 열이나 행의 화면을 각각 flex의 비율로 채우게 한다. 핸드폰 크기가 각자 다른 것에 맞추어 반응형으로 작동하는 것이다.
- container가 차지하는 공간을 row,column과 expanded를 통해 열이나 행 전체를 차지하게 할 수 있다.
Timer
import 'package:flutter/material.dart';
import 'dart:async'; // timer를 불러오기 위한 추가 import
void main() {
runApp(App());
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData( // theme 데이터 다른곳에도 불러오기 위한 빌드업
backgroundColor: const Color(0xFFE7626C),//붉은 배경
textTheme: const TextTheme(
headline1: TextStyle(
color: Color(0xFF232B55),//검은 글자
),
),
cardColor: const Color(0xFFF4EDDB),//흰 카드
),
home: const HomeScreen(),//HomeScreen class 불러오기
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState(); //Stateful이어서 있는 것
}
class _HomeScreenState extends State<HomeScreen> {
int totalSeconds = 1500;
late Timer timer; //Timer를 써서 타이머를 호출한다. 그리고 버튼 누를때 부터 초기화가 진행될 것이기에 late로 나중에 초기화 해주겠다고 선언한다.
void onTick(Timer timer){
setState((){
totalSeconds--;
});
}
void onStartPressed(){
timer = Timer.periodic(
const Duration(seconds:1),
onTick,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,//theme에서 불러오기
body: Column(
children: [ // column의 children으로 flexible이 셋, 각자 화면 비율을 1 :3:1 로 가져간다.
Flexible(
flex: 1,
child: Container( //container 생성 후 테마 받아 타이머 글자 ui 표시
alignment: Alignment.bottomCenter,// row의 아래중앙 정렬
child: Text(
'$totalSeconds',
style: TextStyle(
color: Theme.of(context).cardColor,
fontSize: 89,
fontWeight: FontWeight.w600,
),
),
),
),
Flexible(
flex: 3,
child: Center( //중앙정렬
child: IconButton(
iconSize: 120,
color: Theme.of(context).cardColor,
onPressed: onStartPressed,//아이콘버튼이니 눌렀을때 반응 함수 필요
icon: const Icon(Icons.play_circle_outline),
),
),
),
Flexible(
flex: 1,
child: Row( //컨테이너가 더 넓게 열 전체를 차지하게 하려고 row로 지정
children: [
Expanded( // 그후 expanded로 row의 모든 공간 차지하게 함
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius:BorderRadius.circular(50),//끝 둥글게 만들기
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,//좌우 중앙 정렬
children: [
Text(
'Pomodoros',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Theme.of(context).textTheme.headline1!.color,
),
),
Text(
'0',
style: TextStyle(
fontSize: 58,
fontWeight: FontWeight.w600,
color: Theme.of(context).textTheme.headline1!.color,
),
),
],
),
),
),
],
),
)
],
),
);//return scaffold
}
}
- Timer.periodic( Duration(seconds:1), onTick,); 으로 주기적으로 실행하는 Timer.periodic 안에 몇 초마다 반복할 건지 duration을 설정하고, 그 duration 마다 함수 onTick을 실행시켰다.
- 이때 periodic 안의 함수는 소괄호() 없이 이름만 입력한다.
- onTick 함수는 periodic 안에서 시간마다 실행되기에 파라미터로 타이머인 Timer timer를 받아야 한다. 함수를 리턴받는 콜백함수기에 그렇다.
- ontick에서 SetState를 통해 숫자를 1씩 감소시키고 ui를 업데이트 시켰다.
- 버튼을 눌렀을 때 onStartpressed를 실행시켰다.
- 그래서 타이머는 버튼을 눌렀을때 초기화가 시작되어 작동한다.
Pause Play
import 'package:flutter/material.dart';
import 'dart:async'; // timer를 불러오기 위한 추가 import
void main() {
runApp(App());
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData( // theme 데이터 다른곳에도 불러오기 위한 빌드업
backgroundColor: const Color(0xFFE7626C),//붉은 배경
textTheme: const TextTheme(
headline1: TextStyle(
color: Color(0xFF232B55),//검은 글자
),
),
cardColor: const Color(0xFFF4EDDB),//흰 카드
),
home: const HomeScreen(),//HomeScreen class 불러오기
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState(); //Stateful이어서 있는 것
}
class _HomeScreenState extends State<HomeScreen> {
bool isRunning =false;
int totalSeconds = 1500;
late Timer timer; //Timer를 써서 타이머를 호출한다. 그리고 버튼 누를때 부터 초기화가 진행될 것이기에 late로 나중에 초기화 해주겠다고 선언한다.
void onTick(Timer timer){
setState((){
totalSeconds--;
});
}
void onStartPressed(){
timer = Timer.periodic(
const Duration(seconds:1),
onTick,
);
setState((){
isRunning = true;
});
}
void OnPausePressed(){
timer.cancel();
setState((){
isRunning = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,//theme에서 불러오기
body: Column(
children: [ // column의 children으로 flexible이 셋, 각자 화면 비율을 1 :3:1 로 가져간다.
Flexible(
flex: 1,
child: Container( //container 생성 후 테마 받아 타이머 글자 ui 표시
alignment: Alignment.bottomCenter,// row의 아래중앙 정렬
child: Text(
'$totalSeconds',
style: TextStyle(
color: Theme.of(context).cardColor,
fontSize: 89,
fontWeight: FontWeight.w600,
),
),
),
),
Flexible(
flex: 3,
child: Center( //중앙정렬
child: IconButton(
iconSize: 120,
color: Theme.of(context).cardColor,
onPressed: isRunning? OnPausePressed:
onStartPressed,//아이콘버튼이니 눌렀을때 반응 함수 필요
icon: Icon(isRunning ?
Icons.pause_circle_outline:
Icons.play_circle_outline),
),
),
),
Flexible(
flex: 1,
child: Row( //컨테이너가 더 넓게 열 전체를 차지하게 하려고 row로 지정
children: [
Expanded( // 그후 expanded로 row의 모든 공간 차지하게 함
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius:BorderRadius.circular(50),//끝 둥글게 만들기
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,//좌우 중앙 정렬
children: [
Text(
'Pomodoros',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Theme.of(context).textTheme.headline1!.color,
),
),
Text(
'0',
style: TextStyle(
fontSize: 58,
fontWeight: FontWeight.w600,
color: Theme.of(context).textTheme.headline1!.color,
),
),
],
),
),
),
],
),
)
],
),
);//return scaffold
}
}
- 일시정지 기능을 구현하였다.
- bool isRunning에 따라 아이콘과 눌렀을때 실행되는 함수가 달라지며 함수가 실행될 때마다 참 거짓을 바꿔준다.
- timer.cancel()을 통해 타이머를 멈출 수 있다.
Date Format
import 'package:flutter/material.dart';
import 'dart:async'; // timer를 불러오기 위한 추가 import
void main() {
runApp(App());
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData( // theme 데이터 다른곳에도 불러오기 위한 빌드업
backgroundColor: const Color(0xFFE7626C),//붉은 배경
textTheme: const TextTheme(
headline1: TextStyle(
color: Color(0xFF232B55),//검은 글자
),
),
cardColor: const Color(0xFFF4EDDB),//흰 카드
),
home: const HomeScreen(),//HomeScreen class 불러오기
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState(); //Stateful이어서 있는 것
}
class _HomeScreenState extends State<HomeScreen> {
bool isRunning =false;
static const twentyFive = 1500;
int totalSeconds = twentyFive;
int totalPomodoros =0;
late Timer timer; //Timer를 써서 타이머를 호출한다. 그리고 버튼 누를때 부터 초기화가 진행될 것이기에 late로 나중에 초기화 해주겠다고 선언한다.
void onTick(Timer timer){
if(totalSeconds==0){
setState((){
totalPomodoros++;
isRunning=false;
totalSeconds = twentyFive;
});
timer.cancel();
}else{
setState((){
totalSeconds--;
});
}
}
void onStartPressed(){
timer = Timer.periodic(
const Duration(seconds:1),
onTick,
);
setState((){
isRunning = true;
});
}
void onPausePressed(){
timer.cancel();
setState((){
isRunning = false;
});
}
String format(int seconds){
var duration = Duration(seconds: seconds);
return duration.toString().split(".").first.substring(2,7);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,//theme에서 불러오기
body: Column(
children: [ // column의 children으로 flexible이 셋, 각자 화면 비율을 1 :3:1 로 가져간다.
Flexible(
flex: 1,
child: Container( //container 생성 후 테마 받아 타이머 글자 ui 표시
alignment: Alignment.bottomCenter,// row의 아래중앙 정렬
child: Text(
format(totalSeconds),
style: TextStyle(
color: Theme.of(context).cardColor,
fontSize: 89,
fontWeight: FontWeight.w600,
),
),
),
),
Flexible(
flex: 3,
child: Center( //중앙정렬
child: IconButton(
iconSize: 120,
color: Theme.of(context).cardColor,
onPressed: isRunning? onPausePressed:
onStartPressed,//아이콘버튼이니 눌렀을때 반응 함수 필요
icon: Icon(isRunning ?
Icons.pause_circle_outline:
Icons.play_circle_outline),
),
),
),
Flexible(
flex: 1,
child: Row( //컨테이너가 더 넓게 열 전체를 차지하게 하려고 row로 지정
children: [
Expanded( // 그후 expanded로 row의 모든 공간 차지하게 함
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius:BorderRadius.circular(50),//끝 둥글게 만들기
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,//좌우 중앙 정렬
children: [
Text(
'Pomodoros',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Theme.of(context).textTheme.headline1!.color,
),
),
Text(
'$totalPomodoros',
style: TextStyle(
fontSize: 58,
fontWeight: FontWeight.w600,
color: Theme.of(context).textTheme.headline1!.color,
),
),
],
),
),
),
],
),
)
],
),
);//return scaffold
}
}
- 1500은 고정 상수기에 클래스 내에 귀속시키고, 불변한다는 의미로 static const 처리하였다.
- 타이머가 다 되면 포모도로에 숫자가 1 오르고 타이머는 리셋되며 정지된다.
- Duration(hours: a, minutes: b, seconds: c)로 입력하면 a시 b분 c초 형식의 시간단위로 바꿔준다.. 여기서 원하는 단위만 남기고 써도 된다.
- duration으로 나온 현재 타이머 값을 문자열 포맷팅을 통해 필요한 값만 추출하였다.
5.4강 Code Challenge
import 'package:flutter/material.dart';
import 'dart:async'; // timer를 불러오기 위한 추가 import
void main() {
runApp(App());
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData( // theme 데이터 다른곳에도 불러오기 위한 빌드업
backgroundColor: const Color(0xFFE7626C),//붉은 배경
textTheme: const TextTheme(
headline1: TextStyle(
color: Color(0xFF232B55),//검은 글자
),
),
cardColor: const Color(0xFFF4EDDB),//흰 카드
),
home: const HomeScreen(),//HomeScreen class 불러오기
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState(); //Stateful이어서 있는 것
}
class _HomeScreenState extends State<HomeScreen> {
bool isRunning =false;
static const twentyFive = 1500;
int totalSeconds = twentyFive;
int totalPomodoros =0;
late Timer timer; //Timer를 써서 타이머를 호출한다. 그리고 버튼 누를때 부터 초기화가 진행될 것이기에 late로 나중에 초기화 해주겠다고 선언한다.
void onTick(Timer timer){
if(totalSeconds==0){
setState((){
totalPomodoros++;
isRunning=false;
totalSeconds = twentyFive;
});
timer.cancel();
}else{
setState((){
totalSeconds--;
});
}
}
void onStartPressed(){
timer = Timer.periodic(
const Duration(seconds:1),
onTick,
);
setState((){
isRunning = true;
});
}
void onPausePressed(){
timer.cancel();
setState((){
isRunning = false;
});
}
void onReplayPressed(){
timer.cancel();
setState((){
isRunning=false;
totalSeconds = twentyFive;
});
}
String format(int seconds){
var duration = Duration(seconds: seconds);
return duration.toString().split(".").first.substring(2,7);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,//theme에서 불러오기
body: Column(
children: [ // column의 children으로 flexible이 셋, 각자 화면 비율을 1 :3:1 로 가져간다.
Flexible(
flex: 1,
child: Container( //container 생성 후 테마 받아 타이머 글자 ui 표시
alignment: Alignment.bottomCenter,// row의 아래중앙 정렬
child: Text(
format(totalSeconds),
style: TextStyle(
color: Theme.of(context).cardColor,
fontSize: 89,
fontWeight: FontWeight.w600,
),
),
),
),
Flexible(
flex: 3,
child: Center( //중앙정렬
child: IconButton(
iconSize: 120,
color: Theme.of(context).cardColor,
onPressed: isRunning? onPausePressed:
onStartPressed,//아이콘버튼이니 눌렀을때 반응 함수 필요
icon: Icon(isRunning ?
Icons.pause_circle_outline:
Icons.play_circle_outline),
),
),
),
Flexible(
flex: 1,
child: Center( //중앙정렬
child: IconButton(
iconSize: 120,
color: Theme.of(context).cardColor,
onPressed: onReplayPressed,
icon: const Icon(Icons.replay_outlined),
),
),
),
Flexible(
flex: 1,
child: Row( //컨테이너가 더 넓게 열 전체를 차지하게 하려고 row로 지정
children: [
Expanded( // 그후 expanded로 row의 모든 공간 차지하게 함
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius:BorderRadius.circular(50),//끝 둥글게 만들기
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,//좌우 중앙 정렬
children: [
Text(
'Pomodoros',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Theme.of(context).textTheme.headline1!.color,
),
),
Text(
'$totalPomodoros',
style: TextStyle(
fontSize: 58,
fontWeight: FontWeight.w600,
color: Theme.of(context).textTheme.headline1!.color,
),
),
],
),
),
),
],
),
)
],
),
);//return scaffold
}
}
- Reset 버튼을 만들어 누르면 다시 25분으로 돌아가게 해야 한다.
- 시간을 원래대로 되돌리고 일시정지하는 reset 버튼을 추가했다.
- 아이콘 관련해서 내가 착각하고 있는 부분이 있었다. 전에 ui만 만들 때의 아이콘은 이미 오버라이드 받은 거라 icon: 이미지 명 이었지만, 지금은 그렇지 않아 icon: const Icon(Icons.replay_outlined)과 같이 다 써주어야 했는데, 전에 것을 보고하다가 이 부분에서 잠깐 헤맸다.
반응형