all_box 0.2.1
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
π‘ Synchronous, lightweight and fast key-value storage for Flutter β with crash-safe writes and a pure-Flutter reactive layer.
Table of contents #
- Features
- Installing
- Example App
- Features in detail
- Usage examples
- API
- Design decisions
- Known limitations
- Own benchmark
- Comparison
- When to use it (and when not to)
- Testing
- Documentation
- Other packages by us
π Features #
- πͺΆ 100% synchronous reads. After
init(), everyread<T>()is synchronous β noFuture, noFutureBuilder, no I/O wait on the read path. - π 100% Flutter reactive layer.
AllBoxListenableandAllBoxBuilderare built directly onChangeNotifierandValueListenableβ no external state-management dependency at all. - π‘οΈ Real crash-safety. Every write lands on a
.tmpfile first, then an atomic rename replaces the main file (.db); a.bakof the last good state is kept separately, with automatic two-stage fallback (UTF-8 decoding errors andjsonDecodeerrors). - π Explicit
path, never resolved internally.AllBoxnever importspath_providernor resolves any directory β whoever callsinit()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 realTimer, safe fortestWidgets.
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
pathininit().all_boxnever resolves any directory internally β whoever callsinit()always suppliespath, avoiding any plugin resolution inside the library. initialDataonly applies on a genuine first run. The check is done via the presence of<container>.db/<container>.bakon disk, not in-memory state β a container emptied byerase()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
.tmpfile first, then an atomic rename replaces the main file (.db); a.bakof the last good state is kept separately. - Two-stage read error handling. UTF-8 decoding errors and
jsonDecodeerrors are treated as distinct failure stages, each falling back to.bakbefore 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()calljsonEncodeon the value on the spot, debug-only, and emit a reddebugPrintif it isn't serializable β but never throw or block the write (same permissive behavior asGetStorage). 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:webvia conditional imports β neverdart:html, sincedart:htmlblocks WASM compilation (dart2wasm). - Not isolate-safe. Each
AllBoxkeeps 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 owninit()and they won't see each other's writes until they re-read from disk. File.renamefor 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 |
Reactive state for Flutter with zero dependencies β final count = 0.obs; + Observer(...). |
|
all_validations_br |
Brazilian document validation (CPF, CNPJ, CNH, PIX), input formatters/masks, JWT/UUID/currency/encryption utilities. | |
all_image_compress |
Pure-Dart image compression (JPEG, PNG, GIF, BMP, TIFF, WebP), running in isolates. |
π₯ 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.