티스토리 뷰
직전 포스팅에서 간단한 UI 사용을 익히는 book list 프로젝트를 살펴봤다. 이번에는 state, 상태를 좀 더 직관적으로 이해하고 사용에 익숙해지기 위해 pomodoro 타이머를 만들어 보려고 한다.
Pomodoro Timer는 시간 관리 기법 중 하나로, 25분 집중 시간을 가지고 5분의 쉬는 시간을 가지는 방식이다.
timer를 만들어야 하므로, 정수 타입의 timer 변수를 1초마다 상태 변경을 해줘야 하며, 버튼을 클릭하거나 작업 시간이 끝났을 때 timer가 어떤 상태인지 동적으로 변경해줘야 한다.
화면은 TimerScreen 하나만 생성할 것이며, 가지고 있는 기능은 다음과 같다.
- (시작하기) 타이머 시작, 1초씩 감소, 버튼을 (일시정지, 포기하기)로 변경
- (일시정지) 타이머 일시정지, 버튼을 (계속하기, 포기하기)로 변경
- (계속하기) 타이머 계속하기, 버튼을 (일시정지, 포기하기)로 변경
- (포기하기) 작업 중이던 타이머를 중지하고 초기화
- (휴식하기) 작업 타이머가 끝나면 자동으로 휴식 타이머 작동
- (휴식 종료) 휴식 타이머가 끝나면 초기 화면으로 이동, 알림 창
바로 한번 코드를 보자. 각 메서드나 상태에 대한 설명은 주석으로 코드 내에 작성하였다.
◎main.dart
import 'package:flutter/material.dart';
import 'package:timer/screens/timer_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Pomodoro Timer App',
home: TimerScreen()
);
}
}
◎lib > screens > timer_screen.dart
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:sprintf/sprintf.dart';
import 'package:fluttertoast/fluttertoast.dart';
// 타이머의 상태를 나타내기 위한 Enum
enum TimerStatus {
running,
paused,
stopped,
resting
}
class TimerScreen extends StatefulWidget {
@override
State<StatefulWidget> createState() => _TimerScreenState();
}
class _TimerScreenState extends State<TimerScreen> {
// 집중 시간
static const WORK_SECONDS = 25 * 60;
// 쉬는 시간
static const REST_SECONDS = 5 * 60;
// 타이머 상태
late TimerStatus _timerStatus;
// 타이머 시간
late int _timer;
// pomodoro 횟수
late int _pomodoroCount;
// state 초기화
@override
void initState() {
super.initState();
_timerStatus = TimerStatus.stopped;
_timer = WORK_SECONDS;
_pomodoroCount = 0;
}
@override
Widget build(BuildContext context) {
// 집중 타이머가 돌고 있을 때 버튼 리스트
final List<Widget> _runningButtons = [
ElevatedButton(
child: Text(
_timerStatus == TimerStatus.paused ? '계속하기' : '일시정지', // 일시정지 중? 계속하기 : 일시정지,
style: TextStyle(color: Colors.white, fontSize: 20)
),
style: ElevatedButton.styleFrom(primary: Colors.blue),
onPressed: _timerStatus == TimerStatus.paused ? resume : pause,
),
Padding(
padding: EdgeInsets.all(20),
),
ElevatedButton(
onPressed: stop,
child: Text(
'포기하기',
style: TextStyle(fontSize: 16),
),
style: ElevatedButton.styleFrom(primary: Colors.grey),
)
];
// 타이머 정지 시 버튼
final List<Widget> _stoppedButtons = [
ElevatedButton(
onPressed: run,
child: Text(
'시작하기',
style: TextStyle(color: Colors.white, fontSize: 16),
),
style: ElevatedButton.styleFrom(
primary:
_timerStatus == TimerStatus.resting ? Colors.green : Colors.blue, // 휴식 중? 녹색 : 파란색색
),
)
];
return Scaffold(
appBar: AppBar(
title: Text('Pomodoro Timer')
),
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Container(
height: MediaQuery.of(context).size.height * 0.5,
width: MediaQuery.of(context).size.width * 0.6,
child: Center(
child: Text(
secondToString(_timer),
style: TextStyle(
color: Colors.white,
fontSize: 48,
fontWeight: FontWeight.bold,
),
)
),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _timerStatus == TimerStatus.resting ? Colors.green: Colors.blue, // 휴식 중? 녹색 : 파란색
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children:
_timerStatus == TimerStatus.resting ? const[] : _timerStatus == TimerStatus.stopped ? _stoppedButtons : _runningButtons,
)
],
)
);
}
// timer 숫자를 00:00 으로 표현하기 위한 포맷 함수
String secondToString(int seconds) {
return sprintf("%02d:%02d" , [seconds ~/ 60, seconds % 60]);
}
// 타이머 시작
void run() {
setState(() {
_timerStatus = TimerStatus.running;
print("[=>] " + _timerStatus.toString());
runTimer();
});
}
// 집중 끝 -> 휴식
void rest() {
setState(() {
_timer = REST_SECONDS;
_timerStatus = TimerStatus.resting;
print("[=>] " + _timerStatus.toString());
});
}
// 일시정지
void pause() {
setState(() {
_timerStatus = TimerStatus.paused;
print("[=>] " + _timerStatus.toString());
});
}
// 계속하기
void resume() {
run();
}
// 포기하기
void stop() {
setState(() {
_timer = WORK_SECONDS;
_timerStatus = TimerStatus.stopped;
print("[=>] " + _timerStatus.toString());
});
}
// 타이머 작동을 위한 비동기 함수
void runTimer() async {
Timer.periodic(const Duration(seconds: 1), (Timer t) {
switch (_timerStatus) {
case TimerStatus.paused:
t.cancel();
break;
case TimerStatus.stopped:
t.cancel();
break;
case TimerStatus.running:
if(_timer <= 0) {
showToast("작업 완료!");
rest();
} else {
setState(() {
_timer -= 1;
});
}
break;
case TimerStatus.resting:
if(_timer <= 0) {
setState(() {
_pomodoroCount += 1;
});
showToast("오늘 $_pomodoroCount개의 뽀모도로를 달성했습니다.");
t.cancel();
stop();
} else {
setState(() {
_timer -= 1;
});
}
break;
default:
break;
}
});
}
// context 없이 toast message 를 띄우기 위한 Fluttertoast
void showToast(String message) {
Fluttertoast.showToast(
msg: message,
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
timeInSecForIosWeb: 5,
backgroundColor: Colors.grey,
textColor: Colors.white,
fontSize: 16.0
);
}
}
따로 엄청 어렵거나 하는 코드는 없지만, 조금 생소할 수 있는 부분을 짚고 넘어가면,
1. 타이머 포맷(00:00)을 표시하기 위한 포맷 함수를 좀 더 쉽게 구성하려고 sprintf패키지를 설치하여 사용하였다.
// timer 숫자를 00:00 으로 표현하기 위한 포맷 함수
String secondToString(int seconds) {
return sprintf("%02d:%02d" , [seconds ~/ 60, seconds % 60]);
}
sprintf는 C언어에서의 printf 함수의 포맷팅 방식을 그대로 구현한 패키지이다.
2. 타이머의 배경인 꽉 찬 원을 그려주기 위해 BoxDecoration 위젯을 사용하여 도형을 그려줬다.
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _timerStatus == TimerStatus.resting ? Colors.green: Colors.blue, // 휴식 중? 녹색 : 파란색
),
3. 마지막으로 위 코드에서 확인할 수 있듯, 타이머가 돌아가는 runTimer함수에서 토스트 메시지를 띄워줬다. 그러나 Flutter에서 기본으로 제공하는 Toast를 사용하려면 Context에 접근하여야 하는데 그럴 수 없으니 Context 없이도 Toast를 띄울 수 있는 외부 패키지인 fluttertoast를 사용했다.
// context 없이 toast message 를 띄우기 위한 Fluttertoast
void showToast(String message) {
Fluttertoast.showToast(
msg: message,
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
timeInSecForIosWeb: 5,
backgroundColor: Colors.grey,
textColor: Colors.white,
fontSize: 16.0
);
}
이번 프로젝트는 외부 패키지를 2개 정도 사용하였으니, pubspec.yaml에서 그 부분도 get 해야 한다.
◎pubspec.yaml
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
fluttertoast: "^8.0.7"
실행 결과를 확인해보면, 아래와 같이 타이머가 잘 동작하는 것을 확인할 수 있다.
또한 집중 시간이 모두 끝났을 때, 휴식으로 돌아가는 부분도 문제없이 잘 작동한다.
끝!
'Projects' 카테고리의 다른 글
[Flutter] Data Project: Todo List with Firebase (0) | 2022.07.22 |
---|---|
[Flutter] Data Project: Todo List with SQLite (0) | 2022.07.21 |
[Flutter] 기초 UI 프로젝트: Book list (0) | 2022.07.13 |
[Spring Boot] 심부름 중계 플랫폼: Hermes (0) | 2022.04.17 |
[Web-MVC] Web Service: My pet diary (0) | 2021.06.04 |
- redux-thunk
- Async
- react
- javascript
- 이탈리안 레스토랑
- await
- AsyncStorage
- 인천 구월동 이탈리안 맛집
- redux
- Promise
- 정보보안기사 #실기 #정리
- react-native
- 맛집
- 파니노구스토
- 인천 구월동 맛집
- Total
- Today
- Yesterday