← Назад к вопросам

Реализовать таймер обратного отсчёта

1.3 Junior🔥 81 комментариев
#Flutter виджеты#Асинхронность#Нативная интеграция

Условие

Создайте приложение таймера с обратным отсчётом.

Требования

  1. Возможность установить время (часы, минуты, секунды)
  2. Кнопки: Старт, Пауза, Сброс
  3. Отображение оставшегося времени в формате HH:MM:SS
  4. Уведомление (звук или вибрация) при завершении
  5. Работа в фоновом режиме

Дополнительные баллы

  • Круговой прогресс-бар визуализации
  • Пресеты времени (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, корректной работой в фоне и хранением истории использований.