Virtual Clock
A Flutter package for virtual time manipulation and acceleration. Perfect for testing time-based features like streaks, daily bonuses, and scheduled events without waiting in real-time.
Features
- Time Acceleration - Speed up time by any multiplier (100x, 1000x, etc.)
- Time Travel - Jump to any date/time instantly
- Fast Forward - Skip ahead by any duration
- Pause/Resume - Freeze time for deterministic testing
- Clock Events - Subscribe to onNewHour, atNoon, onNewDay, onWeekStart, onWeekEnd
- HTTP Guard - Control HTTP requests during accelerated time
- Persistence - Virtual time survives app restarts
- Auto-Reset - Automatically resets on app version changes
- Production Safe - Debug-mode only by default, forced to 1x in release builds
- Virtual Timers - Timer wrappers that respect accelerated time
- DateTime Extensions - Convenient extensions for virtual time comparisons
- Debug UI - Built-in TimeMachine widget for visual time control
Use Cases
- Testing Streaks - Test 7-day streak logic in seconds instead of waiting a week
- Daily Bonuses - Verify daily bonus awards without waiting 24 hours
- Subscription Expiry - Test subscription renewal flows instantly
- Scheduled Events - Validate event triggers at specific dates/times
- Time-Sensitive UI - Test countdown timers and time-based UI changes
Installation
Add virtual_clock to your pubspec.yaml:
dependencies:
virtual_clock: ^1.0.0-dev.1
Then run:
flutter pub get
Quick Start
Basic Setup
import 'package:virtual_clock/virtual_clock.dart';
void main() async {
// 1. Initialize the global clock
await VirtualClock.setup(
const ClockConfig(
clockRate: 100, // 100x speed: 1 real minute = 100 virtual minutes
appVersion: '1.0.0+1', // For auto-reset on version changes
),
);
// 3. Use virtual time anywhere
final now = clock.now;
print('Virtual time: $now');
runApp(MyApp());
}
With GetIt (Service Locator Pattern)
import 'package:get_it/get_it.dart';
import 'package:virtual_clock/virtual_clock.dart';
final getIt = GetIt.instance;
Future<void> setupLocator() async {
// Initialize
await VirtualClock.setup(ClockConfig(
clockRate: int.parse(dotenv.env['CLOCK_RATE'] ?? '1'),
appVersion: packageInfo.version,
isProduction: dotenv.env['APP_ENV'] == 'production',
));
// Register ClockService (optional, if you want dependency injection)
getIt.registerSingleton<ClockService>(VirtualClock.service);
}
With Provider
import 'package:provider/provider.dart';
import 'package:virtual_clock/virtual_clock.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<ClockService>.value(
value: VirtualClock.service,
child: MaterialApp(
home: HomePage(),
),
);
}
}
// In your widgets
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final clock = context.watch<ClockService>();
return Text('Current time: ${clock.now}');
}
}
Usage
Getting Virtual Time
Replace DateTime.now() with clock.now:
// Before (real time)
final now = DateTime.now();
// After (virtual time)
final now = clock.now;
Time Travel
Jump to any specific date/time:
// Jump to Christmas 2026
clock.timeTravelTo(DateTime(2026, 12, 25));
// Jump to a specific moment
clock.timeTravelTo(DateTime(2026, 1, 15, 14, 30, 0)); // Jan 15, 2026 2:30 PM
Fast Forward
Skip ahead by a duration:
// Skip ahead one week
clock.fastForward(Duration(days: 7));
// Skip ahead 3 hours
clock.fastForward(Duration(hours: 3));
// Skip to tomorrow (midnight)
final tomorrow = clock.now.add(Duration(days: 1));
clock.timeTravelTo(DateTime(tomorrow.year, tomorrow.month, tomorrow.day));
Pause and Resume
Freeze time for deterministic testing:
clock.pause();
final frozenTime = clock.now;
await Future.delayed(Duration(seconds: 5));
assert(clock.now == frozenTime); // Time hasn't moved!
clock.resume(); // Time continues from where it paused
Reset to Real Time
Sync back to real time while keeping the clock rate:
await clock.reset(); // Back to real time, rate preserved
Dynamic Clock Rate
Adjust the speed of time flow on the fly:
// Set specific rate
clock.setClockRate(500); // 500x speed
// Increase rate (defaults to 2x current)
clock.increaseClockRate(); // 500 -> 1000
clock.increaseClockRate(multiplier: 1.5); // 1000 -> 1500
// Decrease rate (defaults to 0.5x current)
clock.decreaseClockRate(); // 1500 -> 750
Note: Clock rate must be non-negative. Negative rates are automatically clamped to 0. Rates above 100,000 are clamped to 100,000.
Clock Events
Subscribe to time-based events for automatic callbacks when time boundaries are crossed:
Available Events
| Event | Triggers When |
|---|---|
onNewHour |
Hour changes (or day/month/year changes) |
atNoon |
Virtual time crosses 12:00 PM |
onNewDay |
Day changes (midnight) |
onWeekStart |
New week begins (Monday) |
onWeekEnd |
Week ends (Sunday → Monday transition) |
Usage
// Get notified when a new day starts
final unsubscribe = clock.onNewDay.subscribe((time) {
print('New day: ${time.day}/${time.month}/${time.year}');
resetDailyBonuses();
});
// Get notified at noon
clock.atNoon.subscribe((time) => showLunchReminder());
// Get notified on new hour
clock.onNewHour.subscribe((time) {
print('New hour: ${time.hour}:00');
updateHourlyStats();
});
// Get notified when week starts (Monday)
clock.onWeekStart.subscribe((time) => resetWeeklyChallenge());
// Get notified when week ends (Sunday to Monday)
clock.onWeekEnd.subscribe((time) => calculateWeeklyStats());
// Unsubscribe when done
unsubscribe();
Event Properties
// Check if event has subscribers
if (clock.onNewDay.hasSubscribers) {
print('Someone is listening for new days');
}
// Get subscriber count
print('${clock.onNewDay.subscriberCount} listeners');
// Clear all subscribers
clock.onNewDay.clearSubscribers();
HTTP Guard
Control HTTP requests during accelerated time to prevent accidental API calls:
Policies
| Policy | Behavior |
|---|---|
HttpPolicy.block |
Block all requests in accelerated mode (default, safest) |
HttpPolicy.allow |
Allow all requests regardless of clock rate |
HttpPolicy.throttle |
Limit requests per real minute |
Configuration
await VirtualClock.setup(ClockConfig(
clockRate: 100,
httpPolicy: HttpAction.throttle,
httpThrottleLimit: 10, // Max 10 requests per real minute
httpAllowedPatterns: ['/auth/*', '/health'], // Always allowed
httpBlockedPatterns: ['/payments/*'], // Always blocked
onHttpRequestDenied: (path, reason) {
print('Request to $path blocked: $reason');
},
));
Usage in HTTP Client
// Before making a request, check with the guard
final result = clock.guardHttpRequest('/api/users');
if (result.denied) {
print('Request blocked: ${result.reason}');
return; // Don't make the request
}
// Safe to proceed
final response = await http.get('/api/users');
Pattern Matching
Patterns support glob syntax:
*matches any characters?matches single character- Exact paths like
/auth/login
Precedence: blockedPatterns > allowedPatterns > httpPolicy
Virtual Timers
Use VirtualTimer for timers that respect accelerated time:
Periodic Timer
// Check for new day every virtual minute
// At 100x speed, this fires every 0.6 real seconds
final timer = VirtualTimer.periodicWithClock(
Duration(minutes: 1),
(timer) {
if (isNewDay()) {
handleNewDay();
}
},
);
// Don't forget to cancel when done
timer.cancel();
One-Time Timer
// Trigger after 1 virtual hour
// At 100x speed, this fires after 36 real seconds
VirtualTimer.delayedWithClock(
Duration(hours: 1),
() => showReminder(),
);
Async Wait
// Wait for 1 virtual day
// At 100x speed, this completes after ~14.4 real minutes
await VirtualTimer.waitWithClock(Duration(days: 1));
DateTime Extensions
Convenient extensions for working with virtual time:
final someDate = DateTime(2026, 1, 15);
// Check if date is today in virtual time
if (someDate.isVirtualToday()) {
print('This is virtually today!');
}
// Check if date is yesterday in virtual time
if (someDate.isVirtualYesterday()) {
print('This was virtually yesterday!');
}
// Check if date is in the virtual past
if (someDate.isInVirtualPast()) {
print('This is in the virtual past');
}
// Check if date is in the virtual future
if (someDate.isInVirtualFuture()) {
print('This is in the virtual future');
}
// Get difference from virtual now
final diff = someDate.differenceFromVirtualNow();
print('${diff.inDays} virtual days from now');
// Check if different from virtual now (with 1 second tolerance)
if (someDate.isDifferentFromVirtualNow()) {
print('Not the current moment');
}
Debug UI
TimeMachine Widget
A slide-out panel wrapper that provides global time control anywhere in your app. This is the recommended way to add the debug interface.
Basic Usage
Wrap your entire app with the overlay for global access:
TimeMachine(
child: MaterialApp(
home: MyHomeScreen(),
),
)
Features
- Slide-out Panel: Animated slide-in/out from right edge
- Dark Overlay: Semi-transparent background when open (tap to dismiss)
- Drag Gestures: Swipe left/right with velocity-based snapping
- Toggle Button: Persistent button attached to panel edge
- Production Safety: Hidden in release mode unless
forceShow: true
Customization
TimeMachine(
child: MyApp(),
panelWidth: 200, // Width of the slide-out panel
theme: TimeControlTheme(...), // Panel theming
themeMode: TimeControlThemeMode.dark,
forceShow: true, // Show even when clockRate == 1
overlayColor: Colors.black54, // Dark overlay color
buttonBuilder: (context, {required isOpen}) {
// Custom toggle button
return Icon(isOpen ? Icons.close : Icons.menu);
},
)
TimeControlPanel (Embedded)
For more complex layouts where you want to embed the controls directly into your own UI (not as an overlay), use TimeControlPanel.
Usage
// Add to your debug settings screen or custom drawer
TimeControlPanel()
Customization
TimeControlPanel(
themeMode: TimeControlThemeMode.dark,
theme: TimeControlTheme(
accentColor: Colors.blue,
),
embedded: true,
showBorder: true,
isOpen: true,
onClose: () => Navigator.pop(context),
)
Clock Configuration
Configuration Options
ClockConfig(
clockRate: 100, // Time multiplier (default: 1, must be >= 0)
isProduction: false, // Force production mode (default: false)
forceEnable: false, // Enable in release/profile mode (default: false)
appVersion: '1.0.0+1', // For auto-reset on version changes
logCallback: (msg, {level = LogLevel.info}) {
// Custom logging
print('[Clock] $msg');
},
// HTTP Guard options
httpPolicy: HttpAction.block,
httpAllowedPatterns: ['/auth/*'],
httpBlockedPatterns: ['/payments/*'],
httpThrottleLimit: 10,
onHttpRequestDenied: (path, reason) => print('Blocked: $path'),
)
Clock Rate Limits
- Minimum: 0 (pauses virtual time progression)
- Maximum: 100,000 (values above are clamped)
- Negative values: Not supported, clamped to 0 with warning
Clock Rate Examples
| Clock Rate | Virtual Speed | 1 Real Minute = | 1 Virtual Day = |
|---|---|---|---|
| 1 | Normal | 1 minute | 24 hours |
| 10 | 10x faster | 10 minutes | 2.4 hours |
| 100 | 100x faster | 1.67 hours | 14.4 minutes |
| 1000 | 1000x faster | 16.67 hours | 86.4 seconds |
| 10000 | 10000x faster | ~1 week | 8.64 seconds |
Environment-Based Configuration
// .env file
CLOCK_RATE=100
APP_ENV=develop
// In your app
await VirtualClock.setup(ClockConfig(
clockRate: int.parse(dotenv.env['CLOCK_RATE'] ?? '1'),
isProduction: dotenv.env['APP_ENV'] == 'production',
appVersion: packageInfo.version,
));
Production Safety
The package includes multiple safety layers:
- Debug Mode Only - Clock rate > 1 only works in debug mode by default
- Release Mode Check - Clock rate is forced to 1 in release builds (
kReleaseMode) - Production Flag - Set
isProduction: trueto reject any acceleration - Force Enable - Set
forceEnable: trueto override debug-mode restriction (use with caution) - Runtime Exception - Throws if acceleration attempted in production mode
- Warning Logs - Prominent banners when acceleration is active
// This will work only in debug mode
await VirtualClock.setup(ClockConfig(clockRate: 100));
// This will throw in production
await VirtualClock.setup(ClockConfig(
clockRate: 100,
isProduction: true, // Will throw!
));
// Force enable in release mode (use with extreme caution!)
await VirtualClock.setup(ClockConfig(
clockRate: 100,
forceEnable: true, // Bypasses debug-mode restriction
));
Constants
The package exports useful constants for customization:
import 'package:virtual_clock/virtual_clock.dart';
// Clock rate limits
kClockRateMin // 0
kClockRateMax // 100,000
kClockRateDefault // 1
// Theme colors (dark theme)
kDarkBackground
kDarkAccent
kDarkTextPrimary
// Theme colors (light theme)
kLightBackground
kLightAccent
kLightTextPrimary
// UI values
kDefaultButtonRadius
kDefaultBadgeRadius
kDefaultTimeFontFamily
Testing
Unit Tests
import 'package:flutter_test/flutter_test.dart';
import 'package:virtual_clock/virtual_clock.dart';
void main() {
late ClockService clockService;
setUp(() async {
SharedPreferences.setMockInitialValues({});
await VirtualClock.setup(ClockConfig(clockRate: 100));
clockService = VirtualClock.service;
});
tearDown(() {
VirtualClock.reset();
});
test('time travel works', () {
final targetDate = DateTime(2026, 12, 25);
clock.timeTravelTo(targetDate);
expect(clock.now.year, 2026);
expect(clock.now.month, 12);
expect(clock.now.day, 25);
});
test('pause freezes time', () async {
clock.pause();
final pausedTime = clock.now;
await Future.delayed(Duration(milliseconds: 100));
expect(clock.now, pausedTime);
});
test('fast forward advances time', () {
final before = clock.now;
clock.fastForward(Duration(days: 7));
final after = clock.now;
expect(after.difference(before).inDays, 7);
});
test('onNewDay event fires on day change', () async {
DateTime? receivedTime;
clock.onNewDay.subscribe((time) => receivedTime = time);
clock.fastForward(Duration(days: 1));
await Future.delayed(Duration(milliseconds: 200));
expect(receivedTime, isNotNull);
});
test('negative clock rate is rejected', () async {
// Negative rates are clamped to 0
clockService.setClockRate(-10);
expect(clockService.clockRate, 0);
});
});
Integration Testing Scenario
// Testing a 7-day streak feature
test('streak unlocks after 7 consecutive days', () async {
// Initialize with 1000x speed (1 virtual day = ~86 seconds)
await VirtualClock.setup(ClockConfig(clockRate: 1000));
// Simulate 7 days of activity
for (int day = 0; day < 7; day++) {
await streakService.recordActivity();
clock.fastForward(Duration(days: 1));
}
// Verify streak achievement
final streak = await streakService.getCurrentStreak();
expect(streak.days, 7);
expect(streak.isUnlocked, true);
});
API Reference
ClockService
| Property | Type | Description |
|---|---|---|
now |
DateTime |
Current virtual time |
clockRate |
int |
Current time multiplier |
isPaused |
bool |
Whether time is paused |
isProduction |
bool |
Whether running in production mode |
isInitialized |
bool |
Whether service has been initialized |
state |
ClockState |
Current state (running/paused) |
lastEventCheckTime |
DateTime? |
Last time events were checked |
| Method | Description |
|---|---|
setup(ClockConfig) |
Initialize the clock service |
timeTravelTo(DateTime) |
Jump to specific date/time |
fastForward(Duration) |
Skip ahead by duration |
pause() |
Freeze time |
resume() |
Unfreeze time |
reset() |
Reset to real time (preserves rate) |
setClockRate(int) |
Change clock rate dynamically |
increaseClockRate({double}) |
Increase rate (defaults to 2x) |
decreaseClockRate({double}) |
Decrease rate (defaults to 0.5x) |
clearAllState() |
Clear all persisted state |
guardHttpRequest(String) |
Check if HTTP request is allowed |
triggerEventCheck() |
Manually trigger event check |
| Event | Description |
|---|---|
onNewHour |
Fires when hour changes |
atNoon |
Fires at 12:00 PM |
onNewDay |
Fires at midnight |
onWeekStart |
Fires when week starts (Monday) |
onWeekEnd |
Fires when week ends (Sunday → Monday) |
VirtualClock (Global Accessor)
| Method | Description |
|---|---|
setup(ClockConfig) |
Initialize both clock service and accessor |
service |
Get the global ClockService |
isInitialized |
Check if initialized |
reset() |
Clear global instance |
VirtualTimer
| Method | Description |
|---|---|
periodic(ClockService, Duration, callback) |
Create periodic timer |
periodicWithClock(Duration, callback) |
Periodic timer using global clock |
delayed(ClockService, Duration, callback) |
Create one-time timer |
delayedWithClock(Duration, callback) |
One-time timer using global clock |
wait(ClockService, Duration) |
Async wait for virtual duration |
waitWithClock(Duration) |
Async wait using global clock |
ClockEvent
| Property/Method | Description |
|---|---|
name |
Event name for debugging |
hasSubscribers |
Whether event has any subscribers |
subscriberCount |
Number of subscribers |
subscribe(callback) |
Subscribe to event, returns unsubscribe function |
unsubscribe(callback) |
Unsubscribe specific callback |
clearSubscribers() |
Remove all subscribers |
HttpGuardResult
| Property | Description |
|---|---|
action |
HttpGuardAction (allow/block/throttle) |
reason |
Reason for blocking (null if allowed) |
allowed |
Whether request is allowed |
denied |
Whether request was denied |
Troubleshooting
Clock not accelerating?
- Check if you're in release mode (
kReleaseModeforces rate to 1) - Verify
isProductionis not set totrue - Ensure
setup()was called before using the clock - In release/profile mode, set
forceEnable: trueif you really need acceleration
Virtual time reset unexpectedly?
The clock auto-resets when the app version changes. This is intentional to prevent stale virtual time from previous development sessions.
Getting StateError: VirtualClock not initialized?
Make sure to call VirtualClock.setup(clockConfig).
Events not firing?
- Ensure you've subscribed before the time change
- Check that time actually crossed the boundary (e.g., for onNewDay, time must cross midnight)
- Wait briefly after fast forward for event timer to trigger
HTTP requests being blocked?
- Check your
httpPolicysetting - Review
httpAllowedPatternsandhttpBlockedPatterns - Use
guardHttpRequest()to check before making requests
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Acknowledgments
Inspired by the need to test time-based features in Flutter apps without the tedious wait times. Built with love for developers who value their time (pun intended).
Libraries
- virtual_clock
- A Flutter package for virtual time manipulation and acceleration.