app_checkpoint 1.0.0 copy "app_checkpoint: ^1.0.0" to clipboard
app_checkpoint: ^1.0.0 copied to clipboard

A Flutter package for checkpointing and restoring app state.

example/lib/main.dart

import 'dart:io';

import 'package:app_checkpoint/checkpoint.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';

void main() {
  assert(() {
    StateSnapshot.enable();
    return true;
  }());

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) => GestureDetector(
    onTap: FocusScope.of(context).unfocus,
    child: MaterialApp(
      title: 'Checkpoint Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.transparent),
        appBarTheme: const AppBarTheme(backgroundColor: Colors.black12),
        scaffoldBackgroundColor: Colors.black12,
        useMaterial3: true,
        inputDecorationTheme: const InputDecorationTheme(
          labelStyle: TextStyle(color: Colors.white),
          enabledBorder: OutlineInputBorder(
            borderSide: BorderSide(color: Colors.white24),
          ),
          focusedBorder: OutlineInputBorder(
            borderSide: BorderSide(color: Colors.white),
          ),
        ),
      ),
      home: const CheckpointExample(title: 'Checkpoint Example'),
    ),
  );
}

/// Example state classes
class UserState {
  int age = 0;
  String name = '';
  String email = '';

  Map<String, dynamic> toJson() => {'name': name, 'age': age, 'email': email};

  void restore(Map<String, dynamic> json) {
    name = json['name'] as String? ?? '';
    age = json['age'] as int? ?? 0;
    email = json['email'] as String? ?? '';
  }
}

class SettingsState {
  String theme = 'dark';
  String language = 'en';
  bool notificationsEnabled = true;

  Map<String, dynamic> toJson() => {
    'theme': theme,
    'language': language,
    'notificationsEnabled': notificationsEnabled,
  };

  void restore(Map<String, dynamic> json) {
    theme = json['theme'] as String? ?? 'dark';
    language = json['language'] as String? ?? 'en';
    notificationsEnabled = json['notificationsEnabled'] as bool? ?? true;
  }
}

class CounterState {
  int count = 0;

  Map<String, dynamic> toJson() => {'count': count};

  void restore(Map<String, dynamic> json) => count = json['count'] as int? ?? 0;
}

class CheckpointExample extends StatefulWidget {
  const CheckpointExample({required this.title, super.key});

  final String title;

  @override
  State<CheckpointExample> createState() => _CheckpointExampleState();
}

class _CheckpointExampleState extends State<CheckpointExample> {
  // State instances
  final UserState _userState = UserState();
  final SettingsState _settingsState = SettingsState();
  final CounterState _counterState = CounterState();

  // Storage for persistence
  SnapshotStorage? _storage;

  // UI state
  String _lastSnapshotJson = '';
  String _statusMessage = 'Ready';
  bool _isLoading = false;
  List<String> _savedSnapshotKeys = [];

  // New v1 features UI state
  final Map<String, bool> _selectedKeysForCapture = {
    'user_state': true,
    'settings_state': true,
    'counter_state': true,
  };
  final Map<String, bool> _selectedKeysForRestore = {
    'user_state': true,
    'settings_state': true,
    'counter_state': true,
  };
  final TextEditingController _bugIdController = TextEditingController();
  final TextEditingController _reporterController = TextEditingController();
  bool _continueOnError = false;
  SnapshotValidationResult? _lastValidationResult;

  @override
  void initState() {
    super.initState();
    _setupCheckpoint();
    _initializeStorage();
  }

  Future<void> _initializeStorage() async {
    // Use SharedPreferences for web, FileStorage for other platforms
    try {
      if (kIsWeb) {
        _storage = SharedPreferencesSnapshotStorage();
      } else {
        final dir = await getApplicationDocumentsDirectory();
        final checkpointDir = Directory('${dir.path}/checkpoints');
        _storage = FileSnapshotStorage(directory: checkpointDir);
      }
      await _loadSavedSnapshots();
    } catch (e) {
      // Fallback to memory storage if storage fails
      debugPrint('Failed to initialize storage: $e');
      _storage = MemorySnapshotStorage();
      await _loadSavedSnapshots();
    }
  }

  Future<void> _loadSavedSnapshots() async {
    if (_storage == null) {
      return;
    }
    final keys = await _storage!.listKeys();
    if (mounted) {
      setState(() => _savedSnapshotKeys = keys);
    }
  }

  void _setupCheckpoint() {
    // Register state contributors
    StateSnapshot.register<UserState>(
      key: 'user_state',
      exporter: () => _userState.toJson(),
      importer: (json) {
        _userState.restore(json);
        setState(() {}); // Update UI
      },
    );

    StateSnapshot.register<SettingsState>(
      key: 'settings_state',
      exporter: () => _settingsState.toJson(),
      importer: (json) {
        _settingsState.restore(json);
        setState(() {}); // Update UI
      },
    );

    StateSnapshot.register<CounterState>(
      key: 'counter_state',
      exporter: () => _counterState.toJson(),
      importer: (json) {
        _counterState.restore(json);
        setState(() {}); // Update UI
      },
    );

    // Setup restore hooks
    StateSnapshot.onBeforeRestore.add(
      () => setState(() => _statusMessage = 'Restoring state...'),
    );

    StateSnapshot.onAfterRestore.add(
      () => setState(() => _statusMessage = 'State restored successfully!'),
    );
  }

  Future<void> _captureSnapshot() async {
    setState(() {
      _isLoading = true;
      _statusMessage = 'Capturing snapshot...';
    });

    try {
      // Build metadata if provided
      Map<String, dynamic>? metadata;
      if (_bugIdController.text.isNotEmpty ||
          _reporterController.text.isNotEmpty) {
        metadata = {};
        if (_bugIdController.text.isNotEmpty) {
          metadata['bug_id'] = _bugIdController.text;
        }
        if (_reporterController.text.isNotEmpty) {
          metadata['reporter'] = _reporterController.text;
        }
        metadata['timestamp'] = DateTime.now().toIso8601String();
      }

      // Get selected keys for capture
      final selectedKeys = _selectedKeysForCapture.entries
          .where((e) => e.value)
          .map((e) => e.key)
          .toList();

      // Capture snapshot with metadata and selective keys
      final snapshot = await StateSnapshot.capture(
        appVersion: '1.0.0',
        metadata: metadata,
        keys: selectedKeys.isEmpty ? null : selectedKeys,
      );
      final jsonString = snapshot.toJsonString(pretty: true);

      // Save to storage with timestamp-based key
      if (_storage != null) {
        final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-');
        final key = 'snapshot_$timestamp';
        await _storage!.save(snapshot, key);
      }

      setState(() {
        _lastSnapshotJson = jsonString;
        _statusMessage = 'Snapshot captured and saved!';
        _isLoading = false;
      });

      await _loadSavedSnapshots();
    } catch (e) {
      setState(() {
        _statusMessage = 'Error: $e';
        _isLoading = false;
      });
    }
  }

  Future<void> _restoreSnapshot([String? key]) async {
    setState(() {
      _isLoading = true;
      _statusMessage = 'Restoring snapshot...';
      _lastValidationResult = null;
    });

    try {
      Snapshot? snapshot;

      if (key != null && _storage != null) {
        // Load from storage
        snapshot = await _storage!.load(key);
        if (snapshot == null) {
          setState(() {
            _statusMessage = 'Snapshot not found: $key';
            _isLoading = false;
          });
          return;
        }
      } else if (_lastSnapshotJson.isNotEmpty) {
        // Use current JSON
        final selectedKeys = _selectedKeysForRestore.entries
            .where((e) => e.value)
            .map((e) => e.key)
            .toList();

        await StateSnapshot.restoreFromJson(
          _lastSnapshotJson,
          keys: selectedKeys.isEmpty ? null : selectedKeys,
          continueOnError: _continueOnError,
        );
        setState(() {
          _statusMessage = 'Snapshot restored successfully!';
          _isLoading = false;
        });
        return;
      } else if (_storage != null) {
        // Try to load the latest
        snapshot = await _storage!.load();
        if (snapshot == null) {
          setState(() {
            _statusMessage = 'No snapshot to restore. Capture one first!';
            _isLoading = false;
          });
          return;
        }
      }

      // Get selected keys for restore
      final selectedKeys = _selectedKeysForRestore.entries
          .where((e) => e.value)
          .map((e) => e.key)
          .toList();

      // Restore from loaded snapshot with partial restore
      await StateSnapshot.restoreFromJson(
        snapshot!.toJsonString(),
        keys: selectedKeys.isEmpty ? null : selectedKeys,
        continueOnError: _continueOnError,
      );
      setState(() {
        _lastSnapshotJson = snapshot!.toJsonString(pretty: true);
        _statusMessage = 'Snapshot restored successfully!';
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _statusMessage = 'Error: $e';
        _isLoading = false;
      });
    }
  }

  Future<void> _validateSnapshot([String? key]) async {
    setState(() {
      _isLoading = true;
      _statusMessage = 'Validating snapshot...';
    });

    try {
      String jsonString;
      if (key != null && _storage != null) {
        final snapshot = await _storage!.load(key);
        if (snapshot == null) {
          setState(() {
            _statusMessage = 'Snapshot not found: $key';
            _isLoading = false;
          });
          return;
        }
        jsonString = snapshot.toJsonString();
      } else if (_lastSnapshotJson.isNotEmpty) {
        jsonString = _lastSnapshotJson;
      } else {
        setState(() {
          _statusMessage = 'No snapshot to validate. Capture one first!';
          _isLoading = false;
        });
        return;
      }

      final validation = await StateSnapshot.validateSnapshot(jsonString);
      setState(() {
        _lastValidationResult = validation;
        if (validation.isValid) {
          _statusMessage = 'Validation passed!';
        } else {
          _statusMessage =
              'Validation failed: ${validation.errors.length} error(s)';
        }
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _statusMessage = 'Validation error: $e';
        _isLoading = false;
      });
    }
  }

  void _toggleStateRegistration(String key) {
    if (SnapshotRegistry.instance.isRegistered(key)) {
      StateSnapshot.unregister(key);
      setState(() {
        _statusMessage = 'Unregistered: $key';
      });
    } else {
      // Re-register based on key
      switch (key) {
        case 'user_state':
          StateSnapshot.register<UserState>(
            key: 'user_state',
            exporter: () => _userState.toJson(),
            importer: (json) {
              _userState.restore(json);
              setState(() {});
            },
          );
          break;
        case 'settings_state':
          StateSnapshot.register<SettingsState>(
            key: 'settings_state',
            exporter: () => _settingsState.toJson(),
            importer: (json) {
              _settingsState.restore(json);
              setState(() {});
            },
          );
          break;
        case 'counter_state':
          StateSnapshot.register<CounterState>(
            key: 'counter_state',
            exporter: () => _counterState.toJson(),
            importer: (json) {
              _counterState.restore(json);
              setState(() {});
            },
          );
          break;
      }
      setState(() {
        _statusMessage = 'Registered: $key';
      });
    }
  }

  Future<void> _deleteSnapshot(String key) async {
    if (_storage == null) {
      return;
    }

    setState(() {
      _isLoading = true;
      _statusMessage = 'Deleting snapshot...';
    });

    try {
      final deleted = await _storage!.delete(key);
      if (deleted) {
        setState(() {
          _statusMessage = 'Snapshot deleted successfully!';
          _isLoading = false;
        });
        await _loadSavedSnapshots();
      } else {
        setState(() {
          _statusMessage = 'Snapshot not found: $key';
          _isLoading = false;
        });
      }
    } catch (e) {
      setState(() {
        _statusMessage = 'Error: $e';
        _isLoading = false;
      });
    }
  }

  void _clearSnapshot() => setState(() {
    _lastSnapshotJson = '';
    _statusMessage = 'Snapshot cleared';
    _lastValidationResult = null;
  });

  @override
  void dispose() {
    _bugIdController.dispose();
    _reporterController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(
      title: Text(widget.title, style: const TextStyle(color: Colors.white)),
    ),
    body: SingleChildScrollView(
      padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          // Status message
          Container(
            decoration: BoxDecoration(
              color: Colors.black12,
              border: Border.all(color: Colors.white24),
            ),
            child: Padding(
              padding: const EdgeInsets.symmetric(
                vertical: 12.0,
                horizontal: 16.0,
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Status',
                    style: Theme.of(
                      context,
                    ).textTheme.titleMedium?.copyWith(color: Colors.white),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    _statusMessage,
                    style: Theme.of(
                      context,
                    ).textTheme.bodyMedium?.copyWith(color: Colors.white),
                  ),
                  if (_isLoading) ...[
                    const SizedBox(height: 8),
                    const LinearProgressIndicator(color: Colors.white),
                  ],
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          Container(
            decoration: BoxDecoration(
              color: Colors.black12,
              border: Border.all(color: Colors.white24),
            ),
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'User State',
                    style: Theme.of(
                      context,
                    ).textTheme.titleMedium?.copyWith(color: Colors.white),
                  ),
                  const SizedBox(height: 8),
                  TextField(
                    style: const TextStyle(color: Colors.white),
                    decoration: const InputDecoration(labelText: 'Name'),
                    onChanged: (value) {
                      _userState.name = value;
                    },
                    controller: TextEditingController(text: _userState.name),
                  ),
                  const SizedBox(height: 8),
                  TextField(
                    decoration: const InputDecoration(labelText: 'Age'),
                    keyboardType: TextInputType.number,
                    style: const TextStyle(color: Colors.white),
                    onChanged: (value) =>
                        _userState.age = int.tryParse(value) ?? 0,
                    controller: TextEditingController(
                      text: _userState.age.toString(),
                    ),
                  ),
                  const SizedBox(height: 8),
                  TextField(
                    decoration: const InputDecoration(labelText: 'Email'),
                    style: const TextStyle(color: Colors.white),
                    onChanged: (value) {
                      _userState.email = value;
                    },
                    controller: TextEditingController(text: _userState.email),
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 8),
          Container(
            decoration: BoxDecoration(
              color: Colors.black12,
              border: Border.all(color: Colors.white24),
            ),
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Settings State',
                    style: Theme.of(
                      context,
                    ).textTheme.titleMedium?.copyWith(color: Colors.white),
                  ),
                  const SizedBox(height: 8),
                  DropdownButtonFormField<String>(
                    style: const TextStyle(color: Colors.white),
                    dropdownColor: Colors.black87,
                    decoration: const InputDecoration(
                      labelText: 'Theme',
                      border: OutlineInputBorder(),
                    ),
                    initialValue: _settingsState.theme,
                    items: const [
                      DropdownMenuItem(value: 'light', child: Text('Light')),
                      DropdownMenuItem(value: 'dark', child: Text('Dark')),
                    ],
                    onChanged: (value) {
                      if (value != null) {
                        setState(() => _settingsState.theme = value);
                      }
                    },
                  ),
                  const SizedBox(height: 8),
                  DropdownButtonFormField<String>(
                    style: const TextStyle(color: Colors.white),
                    dropdownColor: Colors.black87,
                    decoration: const InputDecoration(
                      labelText: 'Language',
                      border: OutlineInputBorder(),
                    ),
                    initialValue: _settingsState.language,
                    items: const [
                      DropdownMenuItem(value: 'en', child: Text('English')),
                      DropdownMenuItem(value: 'es', child: Text('Spanish')),
                      DropdownMenuItem(value: 'fr', child: Text('French')),
                    ],
                    onChanged: (value) {
                      if (value != null) {
                        setState(() => _settingsState.language = value);
                      }
                    },
                  ),
                  const SizedBox(height: 8),
                  SwitchListTile(
                    activeThumbColor: Colors.white,
                    activeTrackColor: Colors.grey[800],
                    inactiveThumbColor: Colors.grey[400],
                    inactiveTrackColor: Colors.grey[200],
                    title: const Text(
                      'Notifications',
                      style: TextStyle(color: Colors.white),
                    ),
                    value: _settingsState.notificationsEnabled,
                    onChanged: (value) => setState(
                      () => _settingsState.notificationsEnabled = value,
                    ),
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 8),
          Container(
            decoration: BoxDecoration(
              color: Colors.black12,
              border: Border.all(color: Colors.white24),
            ),
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Counter State',
                    style: Theme.of(
                      context,
                    ).textTheme.titleMedium?.copyWith(color: Colors.white),
                  ),
                  const SizedBox(height: 16),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      IconButton(
                        onPressed: () => setState(() => _counterState.count--),
                        icon: const Icon(Icons.remove, color: Colors.white),
                      ),
                      Text(
                        '${_counterState.count}',
                        style: Theme.of(context).textTheme.headlineMedium
                            ?.copyWith(color: Colors.white),
                      ),
                      IconButton(
                        onPressed: () {
                          setState(() => _counterState.count++);
                        },
                        icon: const Icon(Icons.add, color: Colors.white),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          // New v1 Features Section
          Container(
            decoration: BoxDecoration(
              color: Colors.black12,
              border: Border.all(color: Colors.white24),
            ),
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'V1 Features',
                    style: Theme.of(
                      context,
                    ).textTheme.titleMedium?.copyWith(color: Colors.white),
                  ),
                  const SizedBox(height: 16),
                  // Metadata input
                  Text(
                    'Metadata (Optional)',
                    style: Theme.of(
                      context,
                    ).textTheme.titleSmall?.copyWith(color: Colors.white70),
                  ),
                  const SizedBox(height: 8),
                  TextField(
                    controller: _bugIdController,
                    style: const TextStyle(color: Colors.white),
                    decoration: const InputDecoration(
                      labelText: 'Bug ID',
                      hintText: 'e.g., BUG-123',
                    ),
                  ),
                  const SizedBox(height: 8),
                  TextField(
                    controller: _reporterController,
                    style: const TextStyle(color: Colors.white),
                    decoration: const InputDecoration(
                      labelText: 'Reporter',
                      hintText: 'e.g., John Doe',
                    ),
                  ),
                  const SizedBox(height: 16),
                  // Selective capture
                  Text(
                    'Select Keys to Capture',
                    style: Theme.of(
                      context,
                    ).textTheme.titleSmall?.copyWith(color: Colors.white70),
                  ),
                  const SizedBox(height: 8),
                  ..._selectedKeysForCapture.entries.map(
                    (entry) => CheckboxListTile(
                      title: Text(
                        entry.key,
                        style: const TextStyle(color: Colors.white),
                      ),
                      value: entry.value,
                      onChanged: (value) => setState(
                        () =>
                            _selectedKeysForCapture[entry.key] = value ?? false,
                      ),
                    ),
                  ),
                  const SizedBox(height: 16),
                  // Selective restore
                  Text(
                    'Select Keys to Restore',
                    style: Theme.of(
                      context,
                    ).textTheme.titleSmall?.copyWith(color: Colors.white70),
                  ),
                  const SizedBox(height: 8),
                  ..._selectedKeysForRestore.entries.map(
                    (entry) => CheckboxListTile(
                      title: Text(
                        entry.key,
                        style: const TextStyle(color: Colors.white),
                      ),
                      value: entry.value,
                      onChanged: (value) => setState(
                        () =>
                            _selectedKeysForRestore[entry.key] = value ?? false,
                      ),
                    ),
                  ),
                  const SizedBox(height: 16),
                  // Continue on error toggle
                  SwitchListTile(
                    title: const Text(
                      'Continue on Error',
                      style: TextStyle(color: Colors.white),
                    ),
                    subtitle: const Text(
                      'Continue restoring other keys even if one fails',
                      style: TextStyle(color: Colors.white60),
                    ),
                    value: _continueOnError,
                    onChanged: (value) =>
                        setState(() => _continueOnError = value),
                  ),
                  const SizedBox(height: 16),
                  // State registration controls
                  Text(
                    'State Registration',
                    style: Theme.of(
                      context,
                    ).textTheme.titleSmall?.copyWith(color: Colors.white70),
                  ),
                  const SizedBox(height: 8),
                  Wrap(
                    spacing: 8,
                    children: ['user_state', 'settings_state', 'counter_state']
                        .map((key) {
                          final isRegistered = SnapshotRegistry.instance
                              .isRegistered(key);
                          return FilterChip(
                            label: Text(key),
                            selected: isRegistered,
                            onSelected: (_) => _toggleStateRegistration(key),
                            selectedColor: Colors.green.withValues(alpha: 0.3),
                            checkmarkColor: Colors.green,
                          );
                        })
                        .toList(),
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          // Action buttons
          Row(
            children: [
              Expanded(
                child: ElevatedButton.icon(
                  onPressed: _isLoading ? null : _captureSnapshot,
                  icon: const Icon(Icons.camera_alt),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.white,
                    foregroundColor: Colors.black,
                  ),
                  label: const Text('Capture'),
                ),
              ),
              const SizedBox(width: 8),
              Expanded(
                child: ElevatedButton.icon(
                  onPressed: _isLoading ? null : _restoreSnapshot,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.white,
                    foregroundColor: Colors.black,
                  ),
                  icon: const Icon(Icons.restore),
                  label: const Text('Restore'),
                ),
              ),
            ],
          ),
          const SizedBox(height: 8),
          Row(
            children: [
              Expanded(
                child: OutlinedButton.icon(
                  onPressed: _isLoading ? null : _validateSnapshot,
                  icon: const Icon(Icons.check_circle, color: Colors.white),
                  style: OutlinedButton.styleFrom(
                    foregroundColor: Colors.white,
                    side: const BorderSide(color: Colors.white24),
                  ),
                  label: const Text('Validate'),
                ),
              ),
              const SizedBox(width: 8),
              Expanded(
                child: OutlinedButton.icon(
                  onPressed: _clearSnapshot,
                  icon: const Icon(Icons.clear, color: Colors.white),
                  style: OutlinedButton.styleFrom(
                    foregroundColor: Colors.white,
                    side: const BorderSide(color: Colors.white24),
                  ),
                  label: const Text('Clear'),
                ),
              ),
            ],
          ),
          // Validation result display
          if (_lastValidationResult != null) ...[
            const SizedBox(height: 16),
            Container(
              decoration: BoxDecoration(
                color: _lastValidationResult!.isValid
                    ? Colors.green.withValues(alpha: 0.2)
                    : Colors.red.withValues(alpha: 0.2),
                border: Border.all(
                  color: _lastValidationResult!.isValid
                      ? Colors.green
                      : Colors.red,
                ),
              ),
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        Icon(
                          _lastValidationResult!.isValid
                              ? Icons.check_circle
                              : Icons.error,
                          color: _lastValidationResult!.isValid
                              ? Colors.green
                              : Colors.red,
                        ),
                        const SizedBox(width: 8),
                        Text(
                          _lastValidationResult!.isValid
                              ? 'Validation Passed'
                              : 'Validation Failed',
                          style: Theme.of(context).textTheme.titleMedium
                              ?.copyWith(
                                color: Colors.white,
                                fontWeight: FontWeight.bold,
                              ),
                        ),
                      ],
                    ),
                    if (_lastValidationResult!.errors.isNotEmpty) ...[
                      const SizedBox(height: 8),
                      Text(
                        'Errors:',
                        style: Theme.of(
                          context,
                        ).textTheme.titleSmall?.copyWith(color: Colors.red),
                      ),
                      ..._lastValidationResult!.errors.map(
                        (error) => Padding(
                          padding: const EdgeInsets.only(left: 16, top: 4),
                          child: Text(
                            '• $error',
                            style: const TextStyle(color: Colors.red),
                          ),
                        ),
                      ),
                    ],
                    if (_lastValidationResult!.warnings.isNotEmpty) ...[
                      const SizedBox(height: 8),
                      Text(
                        'Warnings:',
                        style: Theme.of(
                          context,
                        ).textTheme.titleSmall?.copyWith(color: Colors.orange),
                      ),
                      ..._lastValidationResult!.warnings.map(
                        (warning) => Padding(
                          padding: const EdgeInsets.only(left: 16, top: 4),
                          child: Text(
                            '• $warning',
                            style: const TextStyle(color: Colors.orange),
                          ),
                        ),
                      ),
                    ],
                  ],
                ),
              ),
            ),
          ],
          const SizedBox(height: 16),
          if (_savedSnapshotKeys.isNotEmpty) ...[
            Container(
              decoration: BoxDecoration(
                color: Colors.black12,
                border: Border.all(color: Colors.white24),
              ),
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Saved Snapshots',
                      style: Theme.of(
                        context,
                      ).textTheme.titleMedium?.copyWith(color: Colors.white),
                    ),
                    const SizedBox(height: 8),
                    ..._savedSnapshotKeys.map(
                      (key) => ListTile(
                        title: Text(
                          key,
                          style: const TextStyle(color: Colors.white),
                        ),
                        trailing: Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            IconButton(
                              icon: const Icon(
                                Icons.restore,
                                color: Colors.white,
                              ),
                              onPressed: _isLoading
                                  ? null
                                  : () => _restoreSnapshot(key),
                              tooltip: 'Restore',
                            ),
                            IconButton(
                              icon: const Icon(Icons.delete, color: Colors.red),
                              onPressed: _isLoading
                                  ? null
                                  : () => _deleteSnapshot(key),
                              tooltip: 'Delete',
                            ),
                          ],
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),
          ],

          // Snapshot JSON Display
          if (_lastSnapshotJson.isNotEmpty) ...[
            Container(
              decoration: BoxDecoration(
                color: Colors.black12,
                border: Border.all(color: Colors.white24),
              ),
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Text(
                          'Captured Snapshot',
                          style: Theme.of(context).textTheme.titleMedium
                              ?.copyWith(color: Colors.white),
                        ),
                        IconButton(
                          icon: const Icon(Icons.copy, color: Colors.white),
                          onPressed: () =>
                              ScaffoldMessenger.of(context).showSnackBar(
                                const SnackBar(
                                  content: Text('Snapshot copied to clipboard'),
                                ),
                              ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 8),
                    Container(
                      padding: const EdgeInsets.all(8.0),
                      decoration: BoxDecoration(
                        color: Colors.grey[200],
                        borderRadius: BorderRadius.circular(4),
                      ),
                      child: SelectableText(
                        _lastSnapshotJson,
                        style: const TextStyle(
                          fontFamily: 'monospace',
                          fontSize: 12,
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ],
      ),
    ),
  );
}
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