simple_native_timer 0.1.1
simple_native_timer: ^0.1.1 copied to clipboard
High-precision native timers for Flutter apps on Windows and macOS with background execution support.
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),
],
),
);
}
}