simple_native_timer 0.1.1 copy "simple_native_timer: ^0.1.1" to clipboard
simple_native_timer: ^0.1.1 copied to clipboard

High-precision native timers for Flutter apps on Windows and macOS with background execution support.

example/lib/main.dart

import 'dart:async';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:simple_native_timer/simple_native_timer.dart';
import 'package:window_manager/window_manager.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await windowManager.ensureInitialized();

  const windowOptions = WindowOptions(
    size: Size(700, 720),
    minimumSize: Size(600, 690),
    center: true,
    backgroundColor: Colors.transparent,
    skipTaskbar: false,
    titleBarStyle: TitleBarStyle.normal,
  );

  windowManager.waitUntilReadyToShow(windowOptions, () async {
    await windowManager.show();
    await windowManager.focus();
  });

  runApp(const SimpleNativeTimerDemoApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Simple Native Timer Demo',
      theme: ThemeData.dark(useMaterial3: true),
      home: const TimerDashboardPage(),
    );
  }
}

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

  @override
  State<TimerDashboardPage> createState() => _TimerDashboardPageState();
}

class _TimerDashboardPageState extends State<TimerDashboardPage> {
  SimpleNativeTimer? _timer;
  Duration _interval = const Duration(seconds: 10);
  bool _backgroundMode = false;
  int _tickCount = 0;
  final List<double> _driftSamples = <double>[];
  final List<double> _intervalSamples = <double>[];
  DateTime? _lastTickTime;
  DateTime? _startTime;
  String? _platformVersion;
  bool _isStarting = false;

  static final DateFormat _timeFormat = DateFormat('HH:mm:ss.SSS');

  @override
  void initState() {
    super.initState();
    _loadPlatformVersion();
  }

  Future<void> _loadPlatformVersion() async {
    try {
      final version = await SimpleNativeTimer.platformVersion;
      if (mounted) {
        setState(() => _platformVersion = version);
      }
    } catch (error) {
      if (mounted) {
        setState(() => _platformVersion = 'Error: $error');
      }
    }
  }

  Future<void> _startTimer() async {
    if (_timer != null || _isStarting) {
      return;
    }
    setState(() {
      _isStarting = true;
      _tickCount = 0;
      _driftSamples.clear();
      _intervalSamples.clear();
      _lastTickTime = null;
      _startTime = DateTime.now();
    });

    try {
      final timer = await SimpleNativeTimer.periodic(
        _interval,
        _handleTick,
        backgroundMode: _backgroundMode,
      );
      if (!mounted) {
        await timer.cancel();
        return;
      }
      setState(() {
        _timer = timer;
      });
    } catch (error) {
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Failed to start timer: $error')));
    } finally {
      if (mounted) {
        setState(() => _isStarting = false);
      }
    }
  }

  Future<void> _stopTimer() async {
    final timer = _timer;
    if (timer == null) {
      return;
    }
    await timer.cancel();
    if (!mounted) {
      return;
    }
    setState(() => _timer = null);
  }

  FutureOr<void> _handleTick(SimpleNativeTimerTick tick) {
    final now = DateTime.now();
    final lastTick = _lastTickTime;
    if (lastTick != null) {
      final interval = now.difference(lastTick).inMicroseconds / 1000.0;
      _intervalSamples.add(interval);
    }
    _lastTickTime = now;

    final driftMs = tick.drift.inMicroseconds / 1000.0;
    _driftSamples.add(driftMs);

    if (_driftSamples.length > 200) {
      _driftSamples.removeAt(0);
    }
    if (_intervalSamples.length > 200) {
      _intervalSamples.removeAt(0);
    }

    setState(() {
      _tickCount += 1;
    });
  }

  double _calculateAverage(List<double> values) {
    if (values.isEmpty) {
      return 0;
    }
    return values.reduce((a, b) => a + b) / values.length;
  }

  double _calculateJitter(List<double> values) {
    if (values.length < 2) {
      return 0;
    }
    final avg = _calculateAverage(values);
    final variance =
        values.map((v) => pow(v - avg, 2) as double).reduce((a, b) => a + b) /
        (values.length - 1);
    return sqrt(variance);
  }

  @override
  void dispose() {
    unawaited(_timer?.cancel());
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final running = _timer != null;
    return Scaffold(
      appBar: AppBar(
        title: const Text('Simple Native Timer Demo'),
        actions: [
          if (_platformVersion != null)
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: Center(
                child: Text(
                  _platformVersion!,
                  style: Theme.of(context).textTheme.labelMedium?.copyWith(
                    color: Theme.of(context).colorScheme.onPrimaryContainer,
                  ),
                ),
              ),
            ),
        ],
      ),
      body: LayoutBuilder(
        builder: (context, constraints) {
          return SingleChildScrollView(
            padding: const EdgeInsets.all(24),
            child: ConstrainedBox(
              constraints: BoxConstraints(
                minHeight: constraints.maxHeight - 48,
              ).tighten(width: constraints.maxWidth - 48),
              child: IntrinsicHeight(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        Expanded(
                          child: _IntervalSelector(
                            interval: _interval,
                            onChanged:
                                running
                                    ? null
                                    : (duration) =>
                                        setState(() => _interval = duration),
                          ),
                        ),
                        const SizedBox(width: 16),
                        FilterChip(
                          label: const Text('Background mode'),
                          selected: _backgroundMode,
                          onSelected:
                              running
                                  ? null
                                  : (value) =>
                                      setState(() => _backgroundMode = value),
                        ),
                      ],
                    ),
                    const SizedBox(height: 24),
                    _StatisticCard(
                      title: 'Tick Count',
                      value: '$_tickCount',
                      subtitle:
                          _startTime == null
                              ? 'Timer not started'
                              : 'Since ${_timeFormat.format(_startTime!)}',
                    ),
                    const SizedBox(height: 16),
                    _StatisticCard(
                      title: 'Average Drift',
                      value:
                          '${_calculateAverage(_driftSamples).toStringAsFixed(3)} ms',
                      subtitle:
                          'Jitter: ${_calculateJitter(_driftSamples).toStringAsFixed(3)} ms',
                    ),
                    const SizedBox(height: 16),
                    _StatisticCard(
                      title: 'Average Interval',
                      value:
                          '${_calculateAverage(_intervalSamples).toStringAsFixed(2)} ms',
                      subtitle:
                          'Jitter: ${_calculateJitter(_intervalSamples).toStringAsFixed(2)} ms',
                    ),
                    const Spacer(),
                    Row(
                      children: [
                        Expanded(
                          child: ElevatedButton.icon(
                            onPressed:
                                running
                                    ? null
                                    : (_isStarting ? null : _startTimer),
                            icon: const Icon(Icons.play_arrow),
                            label: const Text('Start'),
                          ),
                        ),
                        const SizedBox(width: 16),
                        Expanded(
                          child: ElevatedButton.icon(
                            onPressed: running ? _stopTimer : null,
                            icon: const Icon(Icons.stop),
                            label: const Text('Stop'),
                          ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 12),
                    const Text(
                      'Background mode keeps the CPU awake and may impact battery life.',
                      style: TextStyle(fontStyle: FontStyle.italic),
                    ),
                  ],
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

class _IntervalSelector extends StatelessWidget {
  const _IntervalSelector({required this.interval, required this.onChanged});

  final Duration interval;
  final ValueChanged<Duration>? onChanged;

  @override
  Widget build(BuildContext context) {
    final options = <Duration>[
      const Duration(seconds: 1),
      const Duration(seconds: 5),
      const Duration(seconds: 10),
      const Duration(seconds: 30),
      const Duration(minutes: 1),
    ];
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('Interval', style: Theme.of(context).textTheme.titleMedium),
        const SizedBox(height: 8),
        Wrap(
          spacing: 8,
          children: [
            for (final option in options)
              ChoiceChip(
                label: Text('${option.inSeconds}s'),
                selected: interval == option,
                onSelected:
                    onChanged == null ? null : (_) => onChanged!(option),
              ),
          ],
        ),
      ],
    );
  }
}

class _StatisticCard extends StatelessWidget {
  const _StatisticCard({
    required this.title,
    required this.value,
    required this.subtitle,
  });

  final String title;
  final String value;
  final String subtitle;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(12),
        color: Theme.of(
          context,
        ).colorScheme.surfaceContainerHighest.withValues(alpha: 0.4),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(title, style: Theme.of(context).textTheme.labelLarge),
          const SizedBox(height: 8),
          Text(
            value,
            style: Theme.of(
              context,
            ).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 4),
          Text(subtitle, style: Theme.of(context).textTheme.bodySmall),
        ],
      ),
    );
  }
}
1
likes
150
points
323
downloads

Publisher

unverified uploader

Weekly Downloads

High-precision native timers for Flutter apps on Windows and macOS with background execution support.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on simple_native_timer

Packages that implement simple_native_timer