all_box 0.2.1 copy "all_box: ^0.2.1" to clipboard
all_box: ^0.2.1 copied to clipboard

Synchronous, lightweight key-value storage for Flutter with crash-safe writes (write-ahead + atomic rename) and a pure-Flutter reactive layer.

all_box

πŸ‡§πŸ‡· PortuguΓͺs | πŸ‡ΊπŸ‡Έ English

pub version pub likes pub points license 19 tests

πŸ’‘ Synchronous, lightweight and fast key-value storage for Flutter β€” with crash-safe writes and a pure-Flutter reactive layer.

Table of contents #

πŸš€ Features #

  • πŸͺΆ 100% synchronous reads. After init(), every read<T>() is synchronous β€” no Future, no FutureBuilder, no I/O wait on the read path.
  • πŸ”Œ 100% Flutter reactive layer. AllBoxListenable and AllBoxBuilder are built directly on ChangeNotifier and ValueListenable β€” no external state-management dependency at all.
  • πŸ›‘οΈ Real crash-safety. Every write lands on a .tmp file first, then an atomic rename replaces the main file (.db); a .bak of the last good state is kept separately, with automatic two-stage fallback (UTF-8 decoding errors and jsonDecode errors).
  • πŸ“ Explicit path, never resolved internally. AllBox never imports path_provider nor resolves any directory β€” whoever calls init() decides where the container lives. This avoids, by construction, the plugin/Activity resolution bugs that affect libraries that resolve the path by default.
  • ⚑ Optimistic, debounced writes, with writeAndFlush()/flushNow() for the moments you need a real, immediate on-disk guarantee.
  • πŸ§ͺ In-memory backend for testing. AllBox.initWithMemoryBackendForTesting() runs with no real I/O and no real Timer, safe for testWidgets.

Part of the all_* family of open-source packages alongside all_validations_br (Brazilian validations, utilities and encryption) and all_image_compress (image compression).

πŸ“¦ Installing #

flutter pub add all_box
dependencies:
  all_box: ^0.2.1
import 'package:all_box/all_box.dart';

πŸ“± Example App #

The example/ directory contains an interactive Flutter app (CounterPage) demonstrating the whole day-to-day public surface: optimistic write() vs. writeAndFlush(), a reactive AllBoxBuilder<T>, listenAll for global side effects (a SnackBar), and flushNow() fired on AppLifecycleState.paused.

To run it:

cd example
flutter pub get
flutter run

βš™οΈ Features in detail #

Initialization #

import 'package:all_box/all_box.dart';
import 'package:path_provider/path_provider.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // AllBox never resolves its own directory β€” you do, after the binding is
  // ready. Any path strategy works.
  final dir = await getApplicationDocumentsDirectory();
  await AllBox.init('my_container', path: dir.path);

  runApp(const MyApp());
}

Seeding data on first run (initialData) #

await AllBox.init(
  'settings',
  path: dir.path,
  initialData: const {
    'darkMode': false,
    'onboarded': false,
  },
);

initialData only applies on a genuine first run β€” when the container doesn't yet have <container>.db/<container>.bak on disk. It's persisted immediately (it doesn't wait for the debounce), so it survives a crash right after the app's first launch. If the container already existed before β€” even as an empty {} left by a previous erase() β€” initialData is ignored and whatever is on disk wins.

Reading and writing (every read is synchronous) #

final box = AllBox('my_container');

box.write('name', 'Carlos');           // optimistic: memory + listeners
                                        // update immediately, disk follows
                                        // ~100ms later (debounced)

String? name = box.read<String>('name');
String safeName = box.readOrDefault<String>('name', 'anonymous');

await box.writeAndFlush('name', 'Carlos'); // waits for disk confirmation

box.remove('name');
box.erase(); // clears everything and notifies every listener that existed

await box.flushNow(); // forces a flush now, e.g. on AppLifecycleState.paused

Listening for changes #

box.listenKey('name', () => print('name changed'));
box.removeListenKey('name', callback);

final dispose = box.listenAll(() => print('container changed'));
// later
dispose();

Reactive widgets, no external state-management dependency #

AllBoxBuilder<int>(
  keyName: 'counter',
  builder: (context, value) => Text('${value ?? 0}'),
)

Or build your own ValueListenable with AllBoxListenable<T>:

final counter = AllBoxListenable<int>('counter');
ValueListenableBuilder<int?>(
  valueListenable: counter,
  builder: (context, value, _) => Text('${value ?? 0}'),
);

DI-free .val() helper (optional) #

An opt-in mini state-manager, with no dependency-injection coupling at all:

final darkMode = 'darkMode'.val(false);
print(darkMode.value);
darkMode.value = true;

πŸ§ͺ Usage examples #

Value with a safe fallback #

final box = AllBox('settings');
final theme = box.readOrDefault<String>('theme', 'light');
// Returns 'light' if the 'theme' key doesn't exist yet

Optimistic write vs. confirmed write #

box.write('score', 100);              // memory updated immediately
await box.writeAndFlush('score', 100); // only returns after disk confirms

Reacting to a single key inside a widget #

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

  @override
  Widget build(BuildContext context) {
    return AllBoxBuilder<bool>(
      keyName: 'darkMode',
      builder: (context, value) => Switch(
        value: value ?? false,
        onChanged: (v) => AllBox().write('darkMode', v),
      ),
    );
  }
}

Clearing a container and reacting globally #

final dispose = box.listenAll(() => print('something changed in "settings"'));

box.erase(); // fires the listener above exactly once

dispose();

Container introspection #

box.hasData('theme');   // true / false
box.getKeys();          // every key ever written
box.getValues();        // every value ever written

Persisting app state when paused #

class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused) {
      AllBox('my_container').flushNow();
    }
  }
}

πŸ“š API #

Member Description
AllBox([container]) Factory constructor; returns a singleton per container name.
static AllBox.init(container, {required path, flushDelay, initialData}) Loads container from disk into memory. path is required β€” see below. initialData seeds default values, but only on a genuine first run.
T? read<T>(key) / T readOrDefault<T>(key, fallback) Synchronous reads.
void write(key, value) Optimistic, debounced write. In debug mode, warns (via a red debugPrint) if value isn't JSON-encodable, but never throws.
Future<void> writeAndFlush(key, value) Writes and waits for disk confirmation. Same serialization warning as write().
void remove(key) / void erase() Removes a key / clears everything (erase() notifies every previously-existing key's listeners).
Future<void> flushNow() Forces an immediate flush, bypassing the debounce window.
listenKey(key, cb) / removeListenKey(key, cb) Per-key listeners.
VoidCallback listenAll(cb) Global listener; returns a dispose function.
hasData(key), getKeys(), getValues() Introspection.
AllBoxListenable<T> ChangeNotifier + ValueListenable<T?> for a single key.
AllBoxBuilder<T> Widget that rebuilds when keyName changes.
'key'.val<T>(default) Optional DI-free mini state-manager handle.

Why is path a required parameter of init()? #

AllBox never imports path_provider (nor resolves any directory) internally. The caller always decides where the container lives. It's a deliberate design choice, not an oversight β€” see the section below.

πŸ› οΈ Design decisions #

  • Explicit, required path in init(). all_box never resolves any directory internally β€” whoever calls init() always supplies path, avoiding any plugin resolution inside the library.
  • initialData only applies on a genuine first run. The check is done via the presence of <container>.db/<container>.bak on disk, not in-memory state β€” a container emptied by erase() still has a persisted {}, so it's not considered a "first run" and the seed isn't reapplied over it.
  • Crash-safety via write-ahead + atomic rename. Every disk write lands on a .tmp file first, then an atomic rename replaces the main file (.db); a .bak of the last good state is kept separately.
  • Two-stage read error handling. UTF-8 decoding errors and jsonDecode errors are treated as distinct failure stages, each falling back to .bak before giving up and starting empty.
  • Serialized flush queue. There are never two concurrent writes on the same file, even if flushNow()/writeAndFlush() is called while a debounced flush is still in flight.
  • Own benchmark. Performance numbers measured and maintained in this repository; see benchmark/ and the Comparison section.
  • Debug-only serialization warning, not an exception. write()/writeAndFlush() call jsonEncode on the value on the spot, debug-only, and emit a red debugPrint if it isn't serializable β€” but never throw or block the write (same permissive behavior as GetStorage). The value is still written to memory normally; if it truly can't be encoded, the failure only resurfaces silently deep inside the flush.
  • No Web support in this v1 (see limitations below).

⚠️ Known limitations (documented, not hidden) #

  • No Web support in this v1. If it's ever added, it should use package:web via conditional imports β€” never dart:html, since dart:html blocks WASM compilation (dart2wasm).
  • Not isolate-safe. Each AllBox keeps its state in memory in the isolate where it was initialized; there's no cross-isolate synchronization. If you use multiple isolates (e.g. compute(), background isolates), each one needs its own init() and they won't see each other's writes until they re-read from disk.
  • File.rename for the atomic swap is OS-dependent. On POSIX (Linux/macOS/Android/iOS), renaming over an existing file is atomic. On Windows, behavior can vary between Dart SDK versions; test this scenario specifically if your app runs on Windows desktop.

πŸ“Š Benchmark results (local run) #

Numbers from a real run of benchmark/benchmark.dart (Dart 3.9.2 stable, Windows 11 Pro), confirming the cost of each path described above β€” in-memory reads/writes are orders of magnitude faster than any path that touches disk, and debouncing drastically cuts the cost of write bursts compared to confirming each one to disk individually:

[AllBox benchmark chart: average read/write in memory, debounced write, and durable writeAndFlush]

Β΅s Γ— ms: the chart uses the most readable unit for each bar β€” Β΅s (microsecond, one millionth of a second) for the first three operations, which are memory-only, and ms (millisecond, one thousandth of a second = 1,000 Β΅s) only for writeAndFlush(), which actually touches disk and is therefore orders of magnitude slower. It's not a unit error β€” it's automatic zoom so every bar stays readable.

Operation Throughput Average latency
In-memory read<int>() 1,495,886 ops/s 0.67 Β΅s/op
In-memory write() (optimistic) 92,674 ops/s 10.79 Β΅s/op
200Γ— debounced write() + 1 flushNow() 36,403 ops/s 27.47 Β΅s/op
writeAndFlush() (tmp + backup + rename, per call) 187 ops/s 5.34 ms/op (= 5,340.29 Β΅s/op)

As benchmark/benchmark.dart itself documents, these numbers only hold for the environment I tested on β€” run dart run benchmark/benchmark.dart in your own environment to measure on your own machine/disk.

βš–οΈ Comparison #

all_box GetStorage Hive Isar SharedPreferences
Reads Synchronous, in memory Synchronous, in memory Synchronous (open box) Synchronous (simple) / async (queries) Async
Storage path Explicit, required Resolved internally Resolved by caller Resolved by caller Resolved by platform
Documented crash-safety Write-ahead + atomic rename + .bak Not documented at the same level Internal WAL/compaction WAL via its own engine Platform-dependent
Web support No (v1) Yes Yes Yes Yes
Scope Key-value + reactivity only Storage + some UI utils (GetX) Box-oriented storage Full database Platform wrapper

all_box intentionally doesn't try to be a database or resolve its own path β€” that's a design choice, not a gap. Full, detailed comparison, including a performance benchmark, here.

πŸ€” When to use it (and when not to) #

Reach for all_box when you want simple key-value storage β€” settings, flags, small app state β€” with synchronous reads after boot, optimistic writes with an explicit opt-in to durable confirmation, and a reactive layer with no external state-management dependency.

Reach for something else when you specifically need what it specializes in: Web support and custom type adapters (Hive), a full embedded database with queries/indexes/relations (Isar), or the Flutter ecosystem's most "standard" platform wrapper (SharedPreferences) for a small app that doesn't need built-in reactivity.

πŸ§ͺ Testing #

flutter test

The tests specifically cover the bug scenarios mapped above: a file corrupted with random binary bytes, invalid JSON, fallback to .bak, multiple write() calls coalescing into a single flush, isolation between containers, correct listener notification on erase(), and listenKey/listenAll being correctly removed.

Testing code that consumes all_box #

If you're testing your own app/package (not all_box itself), you don't need a real directory on disk β€” use the in-memory backend:

await AllBox.initWithMemoryBackendForTesting(
  'my_container',
  initialValues: {'darkMode': true},
);

This does no real I/O and schedules no real Timer (every write() "flushes" synchronously) β€” this matters especially inside testWidgets: your FakeAsync zone expects every Timer to resolve before the test ends, and a real disk-backed container would leave a debounce Timer pending there.

πŸ“š Documentation #

  • Comparison β€” detailed comparison vs. GetStorage, Hive, Isar, SharedPreferences, including a performance benchmark.

πŸ“¦ Other packages by us #

all_box is part of a small family of zero/low-dependency Dart & Flutter packages published under the opensource.tatamemaster.com.br verified publisher:

Package Version Description
all_observer pub Reactive state for Flutter with zero dependencies β€” final count = 0.obs; + Observer(...).
all_validations_br pub Brazilian document validation (CPF, CNPJ, CNH, PIX), input formatters/masks, JWT/UUID/currency/encryption utilities.
all_image_compress pub Pure-Dart image compression (JPEG, PNG, GIF, BMP, TIFF, WebP), running in isolates.

πŸ‘₯ Contributors #

Contributors

Made with contrib.rocks.

Contributions are welcome! Read CONTRIBUTING.md to get started.


Issues and pull requests are welcome at the GitHub repository. Distributed under the MIT license.

5
likes
0
points
809
downloads

Publisher

verified publisheropensource.tatamemaster.com.br

Weekly Downloads

Synchronous, lightweight key-value storage for Flutter with crash-safe writes (write-ahead + atomic rename) and a pure-Flutter reactive layer.

Repository (GitHub)
View/report issues

Topics

#storage #key-value #persistence #cache

License

unknown (license)

Dependencies

flutter

More

Packages that depend on all_box