Checkpoint
Flutter State Snapshot & Restore SDK A developer-only Flutter package for capturing, serializing, and restoring logical app state to reproduce bugs and debug state-related issues.
Goal
Provide a dev-only Flutter package that can:
- Capture a snapshot of logical app state
- Serialize it deterministically
- Restore it later to reproduce bugs
- Persist snapshots for later use
This is not UI screenshots, not widget trees, not production persistence.
Design Philosophy
State is opt-in, explicit, and owned by the developer.
Checkpoint does NOT crawl memory.
Checkpoint does NOT guess state.
Checkpoint exposes an API that developers consciously wire.
This keeps it:
- ✅ Predictable - You know exactly what's being captured
- ✅ Testable - Explicit contracts make testing straightforward
- ✅ Safe - No magic means no surprises
- ✅ Acceptable to real teams - Transparent and maintainable
Scope (V1)
What V1 Supports
- ✅ App-level state (logical, serializable)
- ✅ Explicit opt-in state registration
- ✅ Deterministic snapshot creation
- ✅ JSON export/import
- ✅ Programmatic restore
- ✅ State validation
- ✅ Persistence utilities (file, memory, and web)
- ✅ Restore lifecycle hooks
- ✅ Partial restore (restore specific keys)
- ✅ Snapshot metadata support
- ✅ Snapshot validation before restore
- ✅ Unregister state contributors
- ✅ Dev / debug only
Getting Started
Prerequisites
- Flutter SDK >= 3.10.7
- Dart SDK >= 3.10.7
Installation
Add app_checkpoint to your pubspec.yaml:
dependencies:
app_checkpoint:
git:
url: https://github.com/vsevex/checkpoint.git
Or when published to pub.dev:
dependencies:
app_checkpoint: ^0.1.0
Quick Start
Enable checkpoint (must be done in an assert block):
void main() {
assert(() {
StateSnapshot.enable();
return true;
}());
runApp(MyApp());
}
Register state contributors:
class UserState {
String name = '';
int age = 0;
Map<String, dynamic> toJson() => {
'name': name,
'age': age,
};
void restore(Map<String, dynamic> json) {
name = json['name'] as String? ?? '';
age = json['age'] as int? ?? 0;
}
}
final userState = UserState();
// Register the state
StateSnapshot.register<UserState>(
key: 'user_state',
exporter: () => userState.toJson(),
importer: (json) => userState.restore(json),
);
// Later, you can unregister if needed
StateSnapshot.unregister('user_state');
Capture a snapshot:
// Capture all state
final snapshot = await StateSnapshot.capture(appVersion: '1.0.0');
// Capture with metadata
final snapshot = await StateSnapshot.capture(
appVersion: '1.0.0',
metadata: {'bug_id': 'BUG-123', 'reporter': 'John'},
);
// Capture only specific keys
final snapshot = await StateSnapshot.capture(
appVersion: '1.0.0',
keys: ['user_state', 'settings_state'],
);
final jsonString = snapshot.toJsonString(pretty: true);
print(jsonString);
Restore from snapshot:
// Restore all state
await StateSnapshot.restoreFromJson(jsonString);
// Restore only specific keys
await StateSnapshot.restoreFromJson(
jsonString,
keys: ['user_state'],
);
// Restore with error tolerance
await StateSnapshot.restoreFromJson(
jsonString,
continueOnError: true, // Continue even if some keys fail
);
// Validate before restoring
final validation = await StateSnapshot.validateSnapshot(jsonString);
if (validation.isValid) {
await StateSnapshot.restoreFromJson(jsonString);
} else {
print('Validation errors: ${validation.errors}');
}
Public API
Enabling Checkpoint
Checkpoint must be enabled in an assert block to ensure zero production overhead:
assert(() {
StateSnapshot.enable();
return true;
}());
In release builds, assert statements are removed, ensuring zero overhead.
Registering State
Register state contributors that define how to export and import state:
StateSnapshot.register<T>(
key: 'unique_key',
exporter: () => Map<String, dynamic>,
importer: (Map<String, dynamic> json) => void,
);
Requirements:
- Keys must be unique
- Exporter must be pure (no side effects, deterministic)
- Importer must be idempotent (safe to call multiple times)
- Both functions must work with JSON-serializable data only
Unregistering State:
You can unregister state contributors when they're no longer needed:
final removed = StateSnapshot.unregister('user_state');
// Returns true if the key was found and removed, false otherwise
Capturing Snapshots
final snapshot = await StateSnapshot.capture(appVersion: '1.0.0');
The snapshot contains:
timestamp- UTC timestamp when capturedappVersion- Version string of the appschemaVersion- Schema version (currently 1)states- Map of all registered state datametadata- Optional custom metadata (if provided)
Parameters:
appVersion- Optional version string (defaults to "0.0.0")metadata- Optional map of custom metadata (must be JSON-serializable)keys- Optional list of keys to capture (if null, captures all registered state)
Deterministic Behavior: State is collected in alphabetical order of keys, ensuring deterministic snapshot creation regardless of registration order.
Examples:
// Capture all state
final snapshot = await StateSnapshot.capture(appVersion: '1.0.0');
// Capture with metadata
final snapshot = await StateSnapshot.capture(
appVersion: '1.0.0',
metadata: {'bug_id': 'BUG-123', 'reporter': 'John'},
);
// Capture only specific keys
final snapshot = await StateSnapshot.capture(
appVersion: '1.0.0',
keys: ['user_state', 'settings_state'],
);
Serializing Snapshots
// Get JSON map
final json = snapshot.toJson();
// Get JSON string (compact)
final jsonString = snapshot.toJsonString();
// Get JSON string (pretty-printed)
final prettyJson = snapshot.toJsonString(pretty: true);
Restoring State
// From JSON string - restore all state
await StateSnapshot.restoreFromJson(jsonString);
// Restore only specific keys
await StateSnapshot.restoreFromJson(
jsonString,
keys: ['user_state', 'settings_state'],
);
// Restore with error tolerance
await StateSnapshot.restoreFromJson(
jsonString,
continueOnError: true, // Continue even if some keys fail
);
// From JSON map
await StateSnapshot.restoreFromJsonMap(jsonMap, keys: ['user_state']);
Parameters:
keys- Optional list of keys to restore (if null, restores all registered state in snapshot)continueOnError- If true, continue restoring other keys even if one fails (defaults to false)
Restore Behavior:
- State is restored in alphabetical order of keys (deterministic)
- All restore operations are logged
- Runs on main isolate
- Blocking/awaitable operation
- Unregistered keys in snapshot are skipped with a warning
Restore Lifecycle Hooks
Add callbacks to be notified before and after restore:
// Before restore starts
StateSnapshot.onBeforeRestore.add(() {
print('About to restore state');
});
// After restore completes
StateSnapshot.onAfterRestore.add(() {
print('State restored successfully');
});
Multiple listeners are supported, and errors in hooks don't prevent restore.
Validating Snapshots
Before restoring a snapshot, you can validate it to check for compatibility issues:
final validation = await StateSnapshot.validateSnapshot(jsonString);
if (validation.isValid) {
await StateSnapshot.restoreFromJson(jsonString);
} else {
print('Validation errors: ${validation.errors}');
print('Warnings: ${validation.warnings}');
}
Validation checks:
- Schema version compatibility
- Whether required state contributors are registered
- Whether snapshot keys exist in the registry
- Warnings for unregistered keys in snapshot
Persistence
Snapshots can be persisted using storage utilities:
In-Memory Storage (for testing)
final storage = MemorySnapshotStorage();
await storage.save(snapshot, 'my_snapshot');
final loaded = await storage.load('my_snapshot');
final keys = await storage.listKeys();
await storage.delete('my_snapshot');
await storage.clear();
File-Based Storage (for desktop/mobile)
import 'package:path_provider/path_provider.dart';
final dir = await getApplicationDocumentsDirectory();
final storage = FileSnapshotStorage(
directory: Directory('${dir.path}/checkpoints'),
);
// Save a snapshot
await storage.save(snapshot, 'my_snapshot');
// Load a snapshot
final loaded = await storage.load('my_snapshot');
if (loaded != null) {
await StateSnapshot.restoreFromJson(loaded.toJsonString());
}
// List all saved snapshots
final keys = await storage.listKeys();
// Delete a snapshot
await storage.delete('my_snapshot');
// Clear all snapshots
await storage.clear();
File Storage Features:
- Automatically creates directory if it doesn't exist
- Saves snapshots as JSON files
- Filters out non-JSON files when listing
- Handles file operations with proper error handling
- Works on Android, iOS, macOS, Windows, and Linux
SharedPreferences Storage (for web and all platforms)
final storage = SharedPreferencesSnapshotStorage();
// Save a snapshot
await storage.save(snapshot, 'my_snapshot');
// Load a snapshot
final loaded = await storage.load('my_snapshot');
if (loaded != null) {
await StateSnapshot.restoreFromJson(loaded.toJsonString());
}
// List all saved snapshots
final keys = await storage.listKeys();
// Delete a snapshot
await storage.delete('my_snapshot');
// Clear all snapshots
await storage.clear();
SharedPreferences Storage Features:
- Works on all platforms including web
- Uses SharedPreferences for persistence
- Keys are prefixed with 'checkpoint_' by default
- Customizable key prefix
- Cross-platform compatibility
Snapshot Data Model
{
"schemaVersion": 1,
"appVersion": "1.2.3",
"timestamp": "2026-01-26T12:00:00.000Z",
"states": {
"user_state": {
"name": "Vsevolod",
"age": 23
},
"settings_state": {
"theme": "dark",
"language": "en"
}
},
"metadata": {
"bug_id": "BUG-123",
"reporter": "John Doe"
}
}
Design principles:
- Simple, flat structure
- No circular references
- JSON-serializable only
- Versioned schema for future compatibility
- Optional metadata for contextual information
Fields:
schemaVersion- Schema version (currently 1)appVersion- Version of the app that created the snapshottimestamp- UTC timestamp when the snapshot was createdstates- Map of state keys to their serialized state datametadata- Optional map of custom metadata (if provided during capture)
State Safety Rules
The package automatically validates exported state to ensure it's JSON-serializable.
✅ Allowed Types
Map<String, dynamic>ListStringnum(int, double)boolnull
❌ Restricted Types
BuildContextor widget references- Streams, Controllers, Futures
- Platform handles (File, Socket, Database, etc.)
- Closures or functions
- Circular references
Validation:
State is automatically validated during capture. If validation fails, a StateError is thrown with a clear error message indicating the problematic path and type.
Example of invalid state:
// ❌ BAD - Contains BuildContext
exporter: () => {'context': context}
// ❌ BAD - Contains Stream
exporter: () => {'stream': myStream}
// ✅ GOOD - Only JSON-serializable data
exporter: () => {'name': 'John', 'age': 30}
Core Architecture
Components
StateSnapshot
Main entry point providing static methods for enabling, registering, capturing, and restoring.
SnapshotRegistry
Central registry mapping string keys to state handlers (exporter/importer pairs). Enforces unique keys and provides thread-safe operations.
SnapshotManager
Orchestrates snapshot capture and restore logic. Coordinates with SnapshotRegistry to collect state in deterministic order.
SnapshotSerializer
Converts snapshots to/from JSON with versioned schema support. Handles schema validation and future migration support.
StateValidator
Validates that exported state is JSON-serializable and safe. Detects non-serializable types and circular references.
SnapshotStorage
Abstract interface for persistence. Implementations include:
MemorySnapshotStorage- In-memory storage for testingFileSnapshotStorage- File-based persistence
Restore Semantics
Restore operations:
- Execute on main isolate
- Are blocking/awaitable
- Restore state in deterministic order (alphabetical by key)
- Log all operations for debugging
- Support lifecycle hooks
Dev-Only Guardrails
Hard guard to ensure zero overhead in release and no accidental prod usage:
assert(() {
StateSnapshot.enable();
return true;
}());
This ensures:
- Zero overhead in release builds (assert statements are removed)
- No accidental production usage
- Explicit opt-in design
Testing
The package includes comprehensive tests covering:
- State registration and validation
- Snapshot capture and serialization
- State restoration and hooks
- Storage persistence (memory and file)
- Error handling and edge cases
Run tests:
flutter test
Example App
See the example/ directory for a complete example app demonstrating:
- Multiple state contributors
- Capture and restore functionality
- Persistence with file storage
- Restore hooks
- Interactive UI for testing
Why This Exists
Debugging state-related bugs in Flutter applications is notoriously difficult. When a user reports a bug, reproducing the exact state that led to the issue is often impossible. Checkpoint solves this by providing a deterministic way to capture and restore application state.
Key benefits:
- Reproduce bugs with exact state snapshots
- Debug state transitions in isolation
- Test state restoration scenarios
- Share bug reproduction cases with team members
- Persist snapshots for later analysis
What makes Checkpoint different:
- Explicit, opt-in design (no magic)
- Developer-controlled state registration
- Deterministic serialization
- Automatic state validation
- Dev-only, zero production overhead
- Flexible persistence options
Limitations
What Checkpoint Cannot Do
- ❌ Capture widget tree state automatically
- ❌ Automatically discover state
- ❌ Handle non-serializable objects (Streams, Controllers, BuildContext, etc.)
- ❌ Work in production builds (dev-only by design)
- ❌ Provide UI for state inspection (use example app or build your own)
- ❌ Handle circular references or complex object graphs
Developer Responsibility
Checkpoint is a tool, not a solution. Developers must:
- Explicitly register state they want to capture
- Ensure state is JSON-serializable
- Handle state restoration in their application logic
- Test restore scenarios thoroughly
- Choose appropriate persistence strategy
The package follows an explicit, opt-in model where developers register state contributors that define how to export and import their application state.
Development Status
This package is in active development. The V1 scope is focused on providing a solid foundation for state snapshot and restore functionality.
Contributing
Contributions are welcome! Please ensure any changes align with the core design philosophy of explicit, opt-in state management.
License
See LICENSE file for details.
Libraries
- checkpoint
- Flutter State Snapshot & Restore SDK
- snapshot
- snapshot_manager
- snapshot_registry
- snapshot_serializer
- snapshot_storage
- snapshot_validation_result
- state_contributor
- state_validator