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.