[Flutter] Pomodoro Timer 만들기

srchae·2025년 1월 19일

🥫Pomodoro란?
25분간 집중해서 일을 한 다음 5분간 휴식하는 방식을 의미한다.
'포모도로'는 이탈리아어로 토마토🍅를 뜻하며,
토마토 모양으로 생긴 요리용 타이머를 이용해 25분간 집중 후 휴식하는
일처리 방법을 제안한데서 유래됐다.

주요 기능

  • 타이머 : 25분(1500초) 동안 타이머를 실행.
  • 일시 정지/재개 : 타이머를 일시 정지하거나 다시 시작.
  • 초기화: 타이머를 25분으로 재설정.
  • Pomodoro 횟수 기록 : 타이머가 끝날 때마다 총 Pomodoro 횟수를 1씩 증가.

main.dart

import 'package:flutter/material.dart';
import 'package:pomodoro/screens/home_screen.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        scaffoldBackgroundColor: const Color(0xFFE7626C),
        textTheme: const TextTheme(
          displayLarge: TextStyle(
            color: Color(0xFF232B55),
          ),
        ),
        cardColor: const Color(0xFFF4EDDB),
      ),
      home: HomeScreen(),
    );
  }
}

우선 전역으로 사용될 Theme를 정의해주었다.

  • 배경색(scaffoldBackgroundColor)
  • 텍스트 테마(textTheme)
  • 카드 색상(cardColor)

그리고 앱의 초기 화면으로 HomeScreen을 설정하여 UI의 시작점을 제공한다.

주요 메서드

onTick()

  • 타이머가 1초 간격으로 호출되며, 남은 시간을 1초씩 감소
  • 시간이 0이 되면 Pomodoro 횟수를 증가시키고 타이머를 초기화
void onTick(Timer timer) {
  if (totalSeconds == 0) {
    setState(() {
      totalPomodoros++;
      totalSeconds = twentyFiveMinutes;
      isRunning = false;
    });
    timer.cancel();
  } else {
    setState(() {
      totalSeconds--;
    });
  }
}

onStartPressed()

  • 타이머를 시작(1초마다 onTick 호출)
  • 실행 상태를 isRunning으로 설정
void onStartPressed() {
  timer = Timer.periodic(Duration(seconds: 1), onTick);
  setState(() {
    isRunning = true;
  });
}

onPausePressed()

  • 타이머를 중지하고 실행 상태를 false로 변경
void onPausePressed() {
  timer.cancel();
  setState(() {
    isRunning = false;
  });
}

onRestartPressed()

  • 타이머를 초기화(25분으로 재설정)하고 러닝 상태 변경
void onRestartPressed() {
  setState(() {
    totalSeconds = twentyFiveMinutes;
  });
}

formatSeconds()

  • 남은 시간을 MM:SS 형태로 변환
String formatSeconds(int seconds) {
  var duration = Duration(seconds: seconds);
  return duration.toString().split('.').first.substring(2, 7);
}

UI 빌드와 관련된 코드는 전체 코드와 함께 추가하겠다.

전체 코드

home_screen.dart

import 'dart:async';

import 'package:flutter/material.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  static const twentyFiveMinutes = 1500;
  int totalSeconds = twentyFiveMinutes; // 25분으로 설정
  // late는 이 속성을 당장 초기화 하지 않아도 된다는 의미
  late Timer timer;
  bool isRunning = false;
  int totalPomodoros = 0;

  // onTick : 타이머를 실행하는 함수
  void onTick(Timer timer) {
    if (totalSeconds == 0) {
      setState(() {
        totalPomodoros = totalPomodoros + 1;
        totalSeconds = twentyFiveMinutes;
        isRunning = false;
      });
      timer.cancel();
    } else {
      setState(() {
        totalSeconds = totalSeconds - 1;
      });
    }
  }

  // onStartPressed : 타이머 초기화를 시작하는 함수 (1초마다 onTick을 실행)
  void onStartPressed() {
    timer = Timer.periodic(Duration(seconds: 1), onTick);
    setState(() {
      isRunning = true;
    });
  }

  void onPausePressed() {
    timer.cancel();
    setState(() {
      isRunning = false;
    });
  }

  String formatSeconds(int seconds) {
    var duration = Duration(seconds: seconds);
    return duration.toString().split('.').first.substring(2, 7);
  }

  void onRestartPressed() {
    setState(() {
      totalSeconds = twentyFiveMinutes;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Theme.of(context).scaffoldBackgroundColor,
      body: Column(
        children: [
          // Flexible : 내부 요소의 크기를 지정하지 않고 유연하게 할당될 수 있도록 한다.
          SizedBox(
            height: 20,
          ),
          Flexible(
              // flex: 요소 하나가 차지할 수 있는 비율을 의미한다.
              flex: 1,
              child: Container(
                alignment: Alignment.bottomCenter,
                child: Text(
                  formatSeconds(totalSeconds),
                  style: TextStyle(
                      color: Theme.of(context).cardColor,
                      fontSize: 89,
                      fontWeight: FontWeight.w600),
                ),
              )),
          Flexible(
            flex: 1,
            child: Container(
              child: Center(
                child: IconButton(
                  onPressed: isRunning ? onPausePressed : onStartPressed,
                  icon: Icon(isRunning
                      ? Icons.pause_circle_outline
                      : Icons.play_circle_outline),
                  iconSize: 120,
                  color: Theme.of(context).cardColor,
                ),
              ),
            ),
          ),
          Flexible(
            flex: 1,
            child: Container(
              child: Center(
                child: IconButton(
                  onPressed: onRestartPressed,
                  icon: Icon(Icons.restart_alt_outlined),
                  iconSize: 120,
                  color: Theme.of(context).cardColor,
                ),
              ),
            ),
          ),
          Flexible(
              flex: 1,
              child: Row(
                children: [
                  Expanded(
                    child: Container(
                      decoration: BoxDecoration(
                        color: Theme.of(context).cardColor,
                        borderRadius: BorderRadius.only(
                          topLeft: Radius.circular(50), // 왼쪽 상단
                          topRight: Radius.circular(50), // 오른쪽 상단
                          bottomLeft: Radius.zero, // 왼쪽 하단 (직선)
                          bottomRight: Radius.zero, // 오른쪽 하단 (직선)
                        ),
                      ),
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Text(
                            "POMODOROS",
                            style: TextStyle(
                                color: Theme.of(context)
                                    .textTheme
                                    .displayLarge!
                                    .color,
                                fontSize: 20,
                                fontWeight: FontWeight.w600),
                          ),
                          Text(
                            " 🍅 $totalPomodoros",
                            style: TextStyle(
                                color: Theme.of(context)
                                    .textTheme
                                    .displayLarge!
                                    .color,
                                fontSize: 60,
                                fontWeight: FontWeight.w600),
                          ),
                        ],
                      ),
                    ),
                  ),
                ],
              ))
        ],
      ),
    );
  }
}

결과물

profile
🐥집요함과 꾸준함🪽

0개의 댓글