app_checkpoint 0.5.0 copy "app_checkpoint: ^0.5.0" to clipboard
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 captured
  • appVersion - Version string of the app
  • schemaVersion - Schema version (currently 1)
  • states - Map of all registered state data
  • metadata - 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 snapshot
  • timestamp - UTC timestamp when the snapshot was created
  • states - Map of state keys to their serialized state data
  • metadata - 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>
  • List
  • String
  • num (int, double)
  • bool
  • null

❌ Restricted Types #

  • BuildContext or 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 testing
  • FileSnapshotStorage - 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.

1
likes
160
points
--
downloads

Publisher

verified publishervsevex.me

Weekly Downloads

A Flutter package for checkpointing and restoring app state.

Repository (GitHub)
View/report issues

Topics

#debugging #restore #state #snapshot

Documentation

API reference

License

MIT (license)

Dependencies

flutter, meta, shared_preferences

More

Packages that depend on app_checkpoint