auto_save_controller

A headless, storage-agnostic Flutter package that extends TextEditingController to automatically save, load, and manage local text drafts — with built-in debounce, TTL (Time-To-Live), and AppLifecycle awareness.

pub.flutter-io.cn License: MIT Live Demo


Features

  • Zero third-party dependencies — only the Flutter SDK.
  • Storage-agnostic — bring your own backend via the AutoSaveStorage interface (SharedPreferences, Hive, Isar, SQLite, in-memory, …).
  • Debounced saves — configurable delay prevents excessive writes during active typing.
  • TTL expiry — drafts older than a configurable threshold are automatically discarded on load.
  • AppLifecycle aware — pending saves are flushed immediately when the app goes inactive, is paused, or detached.
  • Race-condition safe — a draft is never injected if the user typed something during the async load.
  • AutoSaveController typedef — a concise alias for AutoSaveTextEditingController.
  • Drop-in replacement — extends TextEditingController directly; all existing APIs continue to work.

Installation

Add to your pubspec.yaml:

dependencies:
  auto_save_controller: ^1.0.0

Then run:

flutter pub get

Getting Started

Step 1 — Implement AutoSaveStorage

The package ships with no storage implementation so it stays dependency-free. Create your own adapter by implementing the AutoSaveStorage interface:

class InMemoryStorage implements AutoSaveStorage {
  final Map<String, String> _store = {};

  @override
  Future<String?> read(String key) async => _store[key];

  @override
  Future<void> write(String key, String value) async {
    _store[key] = value;
  }

  @override
  Future<void> delete(String key) async => _store.remove(key);
}

See the example app for full implementations using SharedPreferences, Hive, and Isar.

Step 2 — Register Global Storage (optional)

Register a single default backend once at application startup:

void main() {
  AutoSave.init(storage: InMemoryStorage());
  runApp(const MyApp());
}

All controllers that don't receive an explicit storage argument will fall back to this global instance.

Step 3 — Use the Controller

class _MyFormState extends State<MyForm> {
  late final AutoSaveController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AutoSaveController(cacheKey: 'my_form_draft');
  }

  @override
  void dispose() {
    _controller.dispose(); // flushes any pending debounced save
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return TextField(controller: _controller);
  }
}

AutoSaveController is a typedef alias for AutoSaveTextEditingController. Both names refer to the same class.


Per-Controller Storage Injection

Pass a storage argument to the constructor to bypass the global instance for a specific controller. This is useful when different form fields should persist to different backends, or when you want to avoid calling AutoSave.init() entirely:

final controller = AutoSaveController(
  cacheKey: 'account_bio',
  storage: HiveStorage(),          // overrides AutoSave.storage
  debounceDuration: Duration(seconds: 1),
  maxDraftAge: Duration(days: 30),
);

API Reference

AutoSaveController (typedef)

Alias for AutoSaveTextEditingController. Prefer this for brevity.


AutoSaveTextEditingController

Extends TextEditingController.

Parameter Type Default Description
cacheKey String required Unique storage key for the draft.
text String? null Optional initial text.
storage AutoSaveStorage? AutoSave.storage Per-controller storage override.
debounceDuration Duration 500 ms Delay before a save is committed.
maxDraftAge Duration? null TTL — expired drafts are discarded.
maxDraftLength int 10 000 Max chars eligible for saving.
prioritizeDraftOverInitial bool true Draft vs. initial text priority.

Methods

Method Description
clearDraft() Deletes the stored draft and clears the text field.
dispose() Flushes pending saves, disposes lifecycle listener, then calls super.dispose().

AutoSave

Global configuration singleton.

Member Description
AutoSave.init(storage:) Registers the global AutoSaveStorage backend.
AutoSave.storage Returns the registered backend; throws StateError if unset.

AutoSaveStorage

Abstract interface for storage adapters.

abstract class AutoSaveStorage {
  Future<String?> read(String key);
  Future<void> write(String key, String value);
  Future<void> delete(String key);
}

DraftData

Internal model used for serialization. Exposed publicly for custom adapters that need to inspect or migrate stored data.

Property Type Description
text String The draft text content.
timestamp DateTime UTC time the draft was last saved.

Edge Cases

Scenario Behaviour
Storage not initialized AutoSave.storage throws a descriptive StateError.
Draft is null in storage Controller starts empty; no error.
Draft is malformed JSON Silently discarded; debugPrint warning logged.
Draft is expired (TTL) Deleted from storage; controller starts empty.
User types during async load Race condition detected; draft injection aborted.
Initial text provided, prioritizeDraftOverInitial: false Draft ignored; initial text preserved.
Text exceeds maxDraftLength Save skipped; existing draft preserved; warning logged.
Text cleared to empty storage.delete(key) called; storage is cleaned up.
App backgrounded mid-debounce _forceSave() cancels timer and saves immediately.
dispose() with pending timer Timer cancelled; best-effort save fired before teardown.

Example App

Live demo →

The example/ directory contains a full Flutter application demonstrating all three storage adapters (SharedPreferences, Hive, Isar). Each backend has its own page — navigate away and return to see the draft restored from storage.

cd example
flutter pub get
# For Isar — generate the collection schema:
flutter pub run build_runner build --delete-conflicting-outputs
flutter run

Contributing

Contributions, bug reports, and feature requests are welcome! Please open an issue or pull request on GitHub.


License

MIT © auto_save_controller contributors

Libraries

auto_save_controller
Auto-save text editing controller for Flutter.