flutter_form_draft 0.2.0 copy "flutter_form_draft: ^0.2.0" to clipboard
flutter_form_draft: ^0.2.0 copied to clipboard

Automatic form draft saving, encrypted persistence, restoration, versioning, expiration, and lifecycle recovery for Flutter applications.

flutter_form_draft #

Automatic form draft saving, restoration, versioning, expiration, lifecycle recovery, and optional encryption for Flutter applications.

Support on Ko-fi

flutter_form_draft helps prevent data loss when users leave a form early, the app closes, or submission fails. It works with normal Flutter forms and does not require a specific state-management library.

Features #

  • Automatic debounced draft saving
  • Draft restore workflows (automatic, askUser, manual, never)
  • Multiple drafts per form with user isolation
  • Draft expiration and cleanup
  • Schema migrations for evolving forms
  • Pluggable storage backends
  • Authenticated AES-256-GCM payload encryption
  • File storage with atomic writes and optional gzip compression
  • App lifecycle and interval-based saves
  • Field adapters for text, boolean, number, date, enum, list, map, custom model, and file-reference values
  • Built-in restore dialog, autosave indicator, and navigation protection
  • Searchable draft-management screen
  • Checksum-based corruption detection

Installation #

dependencies:
  flutter_form_draft: ^0.2.0

Quick start #

import 'package:flutter/material.dart';
import 'package:flutter_form_draft/flutter_form_draft.dart';

final draftManager = DraftManager(
  storage: SharedPreferencesDraftStorage(),
);

final nameController = TextEditingController();
final emailController = TextEditingController();

final draftController = FormDraftController(
  fields: [
    TextControllerDraftField(key: 'name', controller: nameController),
    TextControllerDraftField(key: 'email', controller: emailController),
  ],
);

FormDraft<Map<String, dynamic>>(
  identity: const DraftIdentity(formId: 'contact_form'),
  manager: draftManager,
  controller: draftController,
  serializer: const MapDraftSerializer(),
  config: const DraftConfig(
    restoreMode: DraftRestoreMode.automatic,
    debounce: Duration(seconds: 1),
  ),
  child: Form(
    child: Column(
      children: [
        TextField(controller: nameController),
        TextField(controller: emailController),
      ],
    ),
  ),
);

How automatic restoration works #

DraftRestoreMode.automatic controls what happens after a saved draft is found. It does not make an in-memory storage backend persistent.

When FormDraft opens, it:

  1. Builds the storage key from formId, draftId, userId, and namespace.
  2. Reads that key from the configured DraftStorage.
  3. Checks expiration, checksum, encryption, and schema version.
  4. Writes the restored values into the registered form controllers.

Automatic restoration therefore requires all of the following:

  • The draft finished saving before the old page was disposed.
  • The new page uses the same DraftIdentity values.
  • The selected storage still contains the draft.
  • The draft has not expired, been discarded, or been deleted after submission.

Returning to a page #

For a small, non-sensitive form that should survive route changes and app restarts, use persistent storage:

final manager = DraftManager(
  storage: SharedPreferencesDraftStorage(),
);

It is safe to call manager.close() when the page is disposed. Closing SharedPreferencesDraftStorage releases the adapter but does not delete its stored drafts.

MemoryDraftStorage is intentionally temporary. Its drafts exist only for the lifetime of that storage instance, and close() clears them. If a page creates its own memory manager and closes it in dispose(), reopening the page creates an empty store and there is nothing to restore.

To keep memory drafts while navigating within the current app session, create one manager above the routes, pass the same manager to each page, and close it only when the owning application scope is disposed. Memory drafts still do not survive an app restart.

Saving before navigation #

DraftSaveMode.onChange waits for the configured debounce period. If custom navigation can happen immediately after typing, flush the pending change first:

Here, draftHandle is the handle supplied by FormDraft.onHandleReady.

await draftHandle.flush();
if (context.mounted) {
  Navigator.of(context).pop();
}

DraftPopScope can handle this for back navigation. Its “Save and leave” path flushes the draft before allowing the route to close.

Storage adapters #

Adapter Best for Platforms
MemoryDraftStorage Tests and drafts limited to one storage-instance lifetime All
SharedPreferencesDraftStorage Small non-sensitive drafts Mobile, web, desktop
FileDraftStorage Large, nested, offline drafts Android, iOS, Windows, macOS, Linux

SharedPreferences persists across route recreation and app restarts, but is not suitable for large payloads or sensitive data.

File storage #

The application chooses the directory, usually with a platform directory package such as path_provider:

final manager = DraftManager(
  storage: FileDraftStorage(
    directoryPath: applicationSupportDirectory.path,
    compress: true,
  ),
);

FileDraftStorage writes one hashed file per draft using temporary-file replacement. It does not support web because browsers do not expose dart:io. Compression must use the same setting when reopening an existing store.

Encryption #

Encryption is optional and works with every storage adapter. Supply a 32-byte key from platform secure storage or another protected key service:

final manager = DraftManager(
  storage: FileDraftStorage(directoryPath: draftsDirectory.path),
  cipher: AesDraftCipher(
    secretKeyProvider: () async => secureKeyStore.loadDraftKey(),
  ),
);

AesDraftCipher uses AES-256-GCM with a new random nonce for every write. Form payloads, identity binding, and checksums are authenticated and encrypted. Custom listing metadata remains readable so management screens can work without decrypting form contents; never put secrets in metadata.

Keys are not generated or persisted by this package. Before rotating a key, read and rewrite existing drafts with the old key or delete them. Losing the key makes encrypted drafts unrecoverable.

Lifecycle and periodic saves #

FormDraft automatically attaches a lifecycle observer for onPause and combined modes:

const DraftConfig(
  saveMode: DraftSaveMode.combined,
  lifecycleSavePolicy: AppLifecycleSavePolicy.pausedDetachedAndHidden,
);

Lifecycle callbacks cannot guarantee enough time for a slow write before the operating system terminates a process. Use on-change or periodic saving as the primary protection for important forms. await handle.flush() remains available for explicit checkpoints.

Periodic saving uses periodicInterval and only writes dirty data:

const DraftConfig(
  saveMode: DraftSaveMode.periodic,
  periodicInterval: Duration(seconds: 30),
);

Advanced field adapters #

Standard Flutter controllers and notifiers remain usable:

final controller = FormDraftController(
  fields: [
    EnumDraftField<Priority>(
      key: 'priority',
      notifier: priorityNotifier,
      values: Priority.values,
    ),
    DateDraftField(key: 'dueDate', notifier: dueDateNotifier),
    ListDraftField<String>(key: 'tags', notifier: tagsNotifier),
    MapDraftField(key: 'address', notifier: addressNotifier),
    ModelDraftField<Address>(
      key: 'shippingAddress',
      notifier: shippingAddressNotifier,
      fromJson: Address.fromJson,
      toJson: (address) => address.toJson(),
    ),
  ],
);

Use DraftFileReference to save durable file metadata rather than binary contents. Temporary picker paths may expire; move files into application-owned storage first. CustomDraftField remains the extension point for signatures, locations, dynamic sections, and other application-specific values.

Restore strategies #

Mode Behavior when a valid draft exists
automatic Immediately writes the draft into registered fields
askUser Shows DraftRestoreDialog or calls onRestoreAvailable
manual Leaves restoration to handle.restoreDraft()
never Leaves the stored draft untouched and does not restore it

The restore mode does not affect how long a draft is stored. Storage lifetime is determined by the selected DraftStorage and manager ownership.

Multiple drafts #

const identity = DraftIdentity(
  formId: 'invoice_form',
  draftId: 'invoice_123',
  userId: 'user_45',
);

Draft management #

DraftManagementScreen searches and filters metadata without loading form payloads. It can rename, duplicate, delete, and clear expired drafts. Restoration is delegated back to the application because only the form knows its model and serializer:

DraftManagementScreen(
  manager: draftManager,
  onRestore: (context, metadata) async {
    await Navigator.of(context).push(
      MaterialPageRoute<void>(
        builder: (_) => EditInvoicePage(identity: metadata.identity),
      ),
    );
  },
);

For a custom interface, use listMetadata, updateMetadata, duplicate, delete, and clearExpired directly.

Schema migrations #

class PropertyDraftV1ToV2 extends DraftMigration {
  @override
  int get fromVersion => 1;

  @override
  int get toVersion => 2;

  @override
  Map<String, dynamic> migrate(Map<String, dynamic> oldData) {
    return {
      ...oldData,
      'propertyType': oldData['type'],
    }..remove('type');
  }
}

final manager = DraftManager(
  storage: MemoryDraftStorage(),
  migrationRegistry: DraftMigrationRegistry([
    PropertyDraftV1ToV2(),
  ]),
);

Example app #

Run the bundled example:

cd example
flutter run

Security considerations #

  • Do not store passwords or secrets in drafts unless excluded or encrypted
  • SharedPreferences is not secure storage
  • Keep encryption keys in platform secure storage; never hardcode production keys
  • Custom metadata is intentionally not encrypted and must remain non-sensitive
  • Encryption cannot guarantee secure deletion on flash storage or synchronized backups

Roadmap #

  • 0.3.0: Retention policies, advanced diagnostics
  • 1.0.0: Stable public API

License #

MIT — see LICENSE.

0
likes
160
points
32
downloads

Documentation

API reference

Publisher

verified publisheramirsaleemahmad.com

Weekly Downloads

Automatic form draft saving, encrypted persistence, restoration, versioning, expiration, and lifecycle recovery for Flutter applications.

Repository (GitHub)
View/report issues
Contributing

Topics

#form #autosave #draft #persistence #offline

License

MIT (license)

Dependencies

collection, crypto, cryptography, flutter, meta, path, shared_preferences

More

Packages that depend on flutter_form_draft