← Назад к вопросам
Реализовать таймер обратного отсчёта
1.3 Junior🔥 81 комментариев
#Flutter виджеты#Асинхронность#Нативная интеграция
Условие
Создайте приложение таймера с обратным отсчётом.
Требования
- Возможность установить время (часы, минуты, секунды)
- Кнопки: Старт, Пауза, Сброс
- Отображение оставшегося времени в формате HH:MM:SS
- Уведомление (звук или вибрация) при завершении
- Работа в фоновом режиме
Дополнительные баллы
- Круговой прогресс-бар визуализации
- Пресеты времени (1 мин, 5 мин, 10 мин)
- История использованных таймеров
- Local Notification при завершении в фоне
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение: Приложение Таймера с Обратным Отсчётом
Архитектура приложения
Таймер будет реализован с использованием State Management (GetX) и Local Notifications для фоновой работы.
Модель данных таймера
// domain/entities/timer_entity.dart
class TimerEntity {
final int totalSeconds;
final int remainingSeconds;
final bool isRunning;
final DateTime createdAt;
final String? name;
TimerEntity({
required this.totalSeconds,
required this.remainingSeconds,
required this.isRunning,
required this.createdAt,
this.name,
});
TimerEntity copyWith({
int? totalSeconds,
int? remainingSeconds,
bool? isRunning,
DateTime? createdAt,
String? name,
}) {
return TimerEntity(
totalSeconds: totalSeconds ?? this.totalSeconds,
remainingSeconds: remainingSeconds ?? this.remainingSeconds,
isRunning: isRunning ?? this.isRunning,
createdAt: createdAt ?? this.createdAt,
name: name ?? this.name,
);
}
}
// Статус таймера
enum TimerStatus { idle, running, paused, completed }
Контроллер таймера
// presentation/controllers/timer_controller.dart
class TimerController extends GetxController {
final LocalNotificationService _notificationService;
final TimerRepository _repository;
late Timer _timer;
final timerStatus = TimerStatus.idle.obs;
final remainingSeconds = 0.obs;
final totalSeconds = 0.obs;
final timerHistory = <TimerEntity>[].obs;
// Пресеты
static const List<int> presets = [60, 300, 600]; // 1, 5, 10 минут
TimerController({
required LocalNotificationService notificationService,
required TimerRepository repository,
})
: _notificationService = notificationService,
_repository = repository;
@override
void onInit() {
super.onInit();
_loadHistory();
_initNotifications();
}
Future<void> _initNotifications() async {
await _notificationService.initialize();
}
/// Установка времени таймера
void setTime(int hours, int minutes, int seconds) {
final total = (hours * 3600) + (minutes * 60) + seconds;
if (total <= 0) {
Get.snackbar(
"Ошибка",
"Время должно быть больше нуля",
snackPosition: SnackPosition.BOTTOM,
);
return;
}
totalSeconds.value = total;
remainingSeconds.value = total;
timerStatus.value = TimerStatus.idle;
}
/// Установка пресета
void setPreset(int seconds) {
setTime(0, 0, seconds);
}
/// Запуск таймера
void start() {
if (totalSeconds.value == 0) {
Get.snackbar(
"Ошибка",
"Установите время перед запуском",
snackPosition: SnackPosition.BOTTOM,
);
return;
}
if (timerStatus.value == TimerStatus.running) return;
timerStatus.value = TimerStatus.running;
_timer = Timer.periodic(Duration(seconds: 1), (_) {
if (remainingSeconds.value > 0) {
remainingSeconds.value--;
} else {
_complete();
}
});
}
/// Пауза
void pause() {
if (timerStatus.value == TimerStatus.running) {
_timer.cancel();
timerStatus.value = TimerStatus.paused;
}
}
/// Возобновление
void resume() {
if (timerStatus.value == TimerStatus.paused) {
start();
}
}
/// Сброс
void reset() {
_timer.cancel();
remainingSeconds.value = totalSeconds.value;
timerStatus.value = TimerStatus.idle;
}
/// Завершение таймера
Future<void> _complete() async {
_timer.cancel();
timerStatus.value = TimerStatus.completed;
remainingSeconds.value = 0;
// Звуковое уведомление
await _notificationService.playSound();
// Вибрация (если поддерживается)
await Vibration.vibrate(
duration: 500,
pattern: [100, 200, 100],
);
// Local Notification для фонового режима
await _notificationService.showNotification(
title: "Таймер завершён",
body: "Ваш таймер закончился",
);
// Сохранение в историю
await _saveToHistory();
// Задержка перед возможностью нового таймера
await Future.delayed(Duration(seconds: 2));
reset();
}
/// Сохранение в историю
Future<void> _saveToHistory() async {
final entry = TimerEntity(
totalSeconds: totalSeconds.value,
remainingSeconds: 0,
isRunning: false,
createdAt: DateTime.now(),
);
timerHistory.add(entry);
await _repository.saveToHistory(entry);
}
/// Загрузка истории
Future<void> _loadHistory() async {
final history = await _repository.getHistory();
timerHistory.assignAll(history);
}
/// Форматирование времени
String get formattedTime {
final hours = remainingSeconds.value ~/ 3600;
final minutes = (remainingSeconds.value % 3600) ~/ 60;
final seconds = remainingSeconds.value % 60;
return '${hours.toString().padLeft(2, "0")}:'
'${minutes.toString().padLeft(2, "0")}:'
'${seconds.toString().padLeft(2, "0")}';
}
/// Прогресс для круговой визуализации (0.0 - 1.0)
double get progress {
if (totalSeconds.value == 0) return 0.0;
return remainingSeconds.value / totalSeconds.value;
}
@override
void onClose() {
_timer.cancel();
super.onClose();
}
}
Сервис Local Notifications
// infrastructure/services/local_notification_service.dart
class LocalNotificationService {
static final LocalNotificationService _instance =
LocalNotificationService._internal();
factory LocalNotificationService() {
return _instance;
}
LocalNotificationService._internal();
final FlutterLocalNotificationsPlugin
_flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
Future<void> initialize() async {
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings("@mipmap/ic_launcher");
const IOSInitializationSettings initializationSettingsIOS =
IOSInitializationSettings(
requestSoundPermission: true,
requestBadgePermission: true,
requestAlertPermission: true,
);
const InitializationSettings initializationSettings =
InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
);
await _flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onDidReceiveNotificationResponse: _onNotificationTap,
);
}
Future<void> showNotification({
required String title,
required String body,
}) async {
const AndroidNotificationDetails androidPlatformChannelSpecifics =
AndroidNotificationDetails(
"timer_channel",
"Timer Notifications",
channelDescription: "Notifications for timer completion",
importance: Importance.max,
priority: Priority.high,
enableVibration: true,
playSound: true,
);
const IOSNotificationDetails iosNotificationDetails =
IOSNotificationDetails(
presentSound: true,
);
const NotificationDetails notificationDetails = NotificationDetails(
android: androidPlatformChannelSpecifics,
iOS: iosNotificationDetails,
);
await _flutterLocalNotificationsPlugin.show(
0,
title,
body,
notificationDetails,
);
}
Future<void> playSound() async {
final audioPlayer = AudioPlayer();
await audioPlayer.play(AssetSource("sounds/timer_complete.mp3"));
}
void _onNotificationTap(NotificationResponse notificationResponse) {
// Обработка нажатия на уведомление
Get.to(() => TimerPage());
}
}
Главная страница таймера
// presentation/pages/timer_page.dart
class TimerPage extends StatefulWidget {
@override
_TimerPageState createState() => _TimerPageState();
}
class _TimerPageState extends State<TimerPage>
with TickerProviderStateMixin {
late AnimationController _rotationController;
final TimerController _timerController = Get.put(TimerController(
notificationService: LocalNotificationService(),
repository: TimerRepository(),
));
@override
void initState() {
super.initState();
_rotationController = AnimationController(
duration: Duration(seconds: 1),
vsync: this,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Таймер"),
elevation: 0,
backgroundColor: Color(0xFF667EEA),
),
body: SingleChildScrollView(
child: Container(
color: Color(0xFFF5F7FA),
child: Column(
children: [
SizedBox(height: 40),
_buildCircularTimer(),
SizedBox(height: 50),
_buildTimeInputSection(),
SizedBox(height: 30),
_buildPresets(),
SizedBox(height: 40),
_buildControlButtons(),
SizedBox(height: 50),
_buildHistory(),
],
),
),
),
);
}
Widget _buildCircularTimer() {
return Obx(() {
return Center(
child: CustomPaint(
size: Size(300, 300),
painter: CircularTimerPainter(
progress: _timerController.progress,
backgroundColor: Colors.white,
progressColor: Color(0xFF667EEA),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_timerController.formattedTime,
style: TextStyle(
fontSize: 56,
fontWeight: FontWeight.bold,
color: Color(0xFF667EEA),
),
),
SizedBox(height: 10),
Text(
_getStatusText(),
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
),
),
);
});
}
Widget _buildTimeInputSection() {
int hours = 0, minutes = 0, seconds = 0;
return Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildTimeInput("Часы", (value) => hours = int.tryParse(value) ?? 0),
_buildTimeInput(
"Минуты", (value) => minutes = int.tryParse(value) ?? 0),
_buildTimeInput(
"Секунды", (value) => seconds = int.tryParse(value) ?? 0),
ElevatedButton(
onPressed: () {
_timerController.setTime(hours, minutes, seconds);
},
child: Icon(Icons.check),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF667EEA),
shape: CircleBorder(),
padding: EdgeInsets.all(15),
),
),
],
),
);
}
Widget _buildTimeInput(
String label,
Function(String) onChanged,
) {
return Column(
children: [
Text(
label,
style: TextStyle(fontSize: 12, color: Colors.grey),
),
SizedBox(height: 8),
SizedBox(
width: 60,
child: TextField(
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
textAlign: TextAlign.center,
decoration: InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(vertical: 8),
),
onChanged: onChanged,
),
),
],
);
}
Widget _buildPresets() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Wrap(
spacing: 10,
children: [
_buildPresetButton("1 мин", 60),
_buildPresetButton("5 мин", 300),
_buildPresetButton("10 мин", 600),
],
),
);
}
Widget _buildPresetButton(String label, int seconds) {
return ElevatedButton(
onPressed: () => _timerController.setPreset(seconds),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
side: BorderSide(color: Color(0xFF667EEA)),
),
child: Text(
label,
style: TextStyle(color: Color(0xFF667EEA)),
),
);
}
Widget _buildControlButtons() {
return Obx(() {
final status = _timerController.timerStatus.value;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (status == TimerStatus.idle || status == TimerStatus.paused)
ElevatedButton.icon(
onPressed: () => _timerController.start(),
icon: Icon(Icons.play_arrow),
label: Text(status == TimerStatus.paused ? "Возобновить" : "Старт"),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF667EEA),
),
),
if (status == TimerStatus.running)
ElevatedButton.icon(
onPressed: () => _timerController.pause(),
icon: Icon(Icons.pause),
label: Text("Пауза"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
),
),
SizedBox(width: 20),
ElevatedButton.icon(
onPressed: () => _timerController.reset(),
icon: Icon(Icons.refresh),
label: Text("Сброс"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[400],
),
),
],
);
});
}
Widget _buildHistory() {
return Obx(() {
if (_timerController.timerHistory.isEmpty) {
return SizedBox.shrink();
}
return Column(
children: [
Text(
"История таймеров",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 10),
ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: _timerController.timerHistory.length,
itemBuilder: (context, index) {
final item = _timerController.timerHistory[index];
return Card(
margin: EdgeInsets.symmetric(horizontal: 20, vertical: 5),
child: ListTile(
title: Text(
"${(item.totalSeconds ~/ 60).toString().padLeft(2, "0")}:${(item.totalSeconds % 60).toString().padLeft(2, "0")}",
),
subtitle: Text(
"${item.createdAt.hour}:${item.createdAt.minute}",
),
),
);
},
),
],
);
});
}
String _getStatusText() {
final status = _timerController.timerStatus.value;
switch (status) {
case TimerStatus.running:
return "Выполняется";
case TimerStatus.paused:
return "На паузе";
case TimerStatus.completed:
return "Завершено";
default:
return "Готов";
}
}
@override
void dispose() {
_rotationController.dispose();
super.dispose();
}
}
Custom Painter для круговой визуализации
// presentation/painters/circular_timer_painter.dart
class CircularTimerPainter extends CustomPainter {
final double progress;
final Color backgroundColor;
final Color progressColor;
CircularTimerPainter({
required this.progress,
required this.backgroundColor,
required this.progressColor,
});
@override
void paint(Canvas canvas, Size size) {
final centerX = size.width / 2;
final centerY = size.height / 2;
final radius = size.width / 2 - 20;
// Фон
canvas.drawCircle(
Offset(centerX, centerY),
radius,
Paint()
..color = backgroundColor
..style = PaintingStyle.stroke
..strokeWidth = 8,
);
// Прогресс
final paint = Paint()
..color = progressColor
..style = PaintingStyle.stroke
..strokeWidth = 8
..strokeCap = StrokeCap.round;
canvas.drawArc(
Rect.fromCircle(center: Offset(centerX, centerY), radius: radius),
-pi / 2,
2 * pi * progress,
false,
paint,
);
}
@override
bool shouldRepaint(CircularTimerPainter oldDelegate) {
return progress != oldDelegate.progress;
}
}
Dependencies в pubspec.yaml
dependencies:
flutter:
sdk: flutter
get: ^4.6.5
flutter_local_notifications: ^16.1.0
vibration: ^1.8.1
audioplayers: ^5.0.0
Этот таймер обеспечивает полный функционал с красивым UI, корректной работой в фоне и хранением истории использований.