app_checkpoint 0.5.0
app_checkpoint: ^0.5.0 copied to clipboard
A Flutter package for checkpointing and restoring app state.
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.