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

Type-safe, FP-style abstraction layers for Hive's boxes. Provides Managers for almost all conceivable use-cases and scenarios.

Package checks Coverage Status PRs Welcome Pub Version Pub Points License: MIT GitHub issues GitHub closed issues GitHub pull requests GitHub closed pull requests

Typed, fpdart-first faΓ§ades over hive_ce boxes. No nulls, no bare Futures, no hand-written CRUD. Pick the box that fits your data and get on with it.

⬆️ Upgrading from 0.0.x? 1.0 is a from-scratch rewrite with a new API, but your data almost always reads in place. MIGRATION.md walks you through it.

Hive is already fast. What it doesn't hand you is a surface that's pleasant to live with, and that's the gap this fills:

  • 🎯 Absence is a type, not null. Reads return an Option (or TaskOption), so "not there" is a case the compiler makes you handle, never a crash waiting to happen.
  • 🧰 CRUD is already written. get, put, update, delete, clear and watch ship on every box, so you stop rewriting the same boilerplate for every type you store.
  • 🧩 Four boxes for four real shapes of data, each in an eager and a lazy flavour, so the box fits the problem instead of the other way round.
  • πŸš€ You keep Hive's speed. The typed surface benchmarks within noise of raw hive_ce, and the numbers below back that up instead of hand-waving it.

Pure Dart, so it runs anywhere Hive does: Flutter apps, Dart servers, CLIs, and the web.

🧭 Which box? #

Start here. Match what you're storing to a family, then grab its eager or lazy variant.

You're storing Reach for Real-world fit
Many values, one key each KeyedBox<T, K> users, todos, cache entries
Exactly one value SingleValueBox<T> a session token, the theme, one config blob
A list of values per key IterableBox<T, K> tags per post, history per day
Values addressed by two parts DualKeyBox<T, K1, K2> (user, day) events, (row, column) grids

Every family has an eager and a Lazy... twin; Eager or lazy? picks the axis with measured numbers. Reverse queries ("everything for this user") live on the dual-key family.

πŸš€ Quickstart #

1. Install #

dart pub add hive_box_manager hive_ce

2. Wire up Hive once #

Engine setup stays hive_ce's, exactly as its docs show:

import 'package:hive_ce/hive.dart';

Hive.init(appDataDirectory.path); // Hive.initFlutter() in Flutter apps
Hive.registerAdapter(TodoAdapter()); // usually generated by hive_ce_generator

None of hive_ce's symbols are re-exported. This package wraps boxes; the engine stays yours.

3. Open a box and use it #

import 'package:hive_box_manager/hive_box_manager.dart';

final todos = await KeyedBox.open<Todo, int>('todos').run();

await todos.put(1, Todo('write the README')).run();

final found = todos.get(1); // Option<Todo>: Some when present, None when absent
final orEmpty = todos.getOr(2, Todo.empty());

todos.watch().listen((event) => print('${event.key} -> ${event.value}'));

That's the whole loop. Reads come straight off the in-memory cache, so they're synchronous; writes are lazy Tasks you run() at the edge. And since you get the box from an open factory, holding one means it's already open. There's no init step to forget.

🧰 The box families #

Here's the good part: all four families wear the same surface, so you learn it once and it carries everywhere. The shape, in short:

  • Absence is always Option / TaskOption, never null and never a magic default.
  • Effects are always Tasks. Nothing touches disk until you .run(), so pipelines compose first and fire once.
  • watch is typed, and close() / deleteFromDisk() are terminal (reacquire, don't reuse).
πŸ“‹ The full method table (KeyedBox shown; the others mirror it)
KeyedBox<T, K> LazyKeyedBox<T, K>
get(key) Option<T> TaskOption<T>
getOr(key, fallback) T Task<T>
values Iterable<T> Task<List<T>>
keys / contains / length sync sync, after first open
put / putAll / delete / deleteAll / clear Task<Unit> Task<Unit>
update(key, fn, {ifAbsent}) Task<T> Task<T>
watch({key}) Stream<TypedBoxEvent<T, K>> Stream<LazyTypedBoxEvent<T, K>>
flush / compact / close / deleteFromDisk Task<Unit> Task<Unit>

Watch is typed on both axes: an eager delete event still carries the value that was removed; a lazy event carries an Option<T> that's None on deletes, because a lazy box holds no value to hand back.

πŸ”‘ KeyedBox #

Many values, each under its own key. The everyday workhorse.

Eager and lazy, side by side

Eager keeps every value in memory once the box opens, so reads are synchronous:

final todos = await KeyedBox.open<Todo, int>('todos').run();

await todos.put(1, Todo('write the README')).run();
final found = todos.get(1); // Option<Todo>

Lazy constructs synchronously and opens itself on the first effect. Values come off disk per read, so reads are effects too:

final archive = LazyKeyedBox<Todo, String>('archive');

await archive.put('2025-w1', Todo('ship 1.0')).run(); // first effect auto-opens
final found = await archive.get('2025-w1').run(); // TaskOption<Todo>
await archive.ensureInitialised().run(); // optional explicit warm-up

πŸ“ SingleValueBox #

One value, no keys. A token, a theme, one config blob.

Set, read, clear, watch
final session = await SingleValueBox.open<String>('session_token').run();

await session.set('abc123').run();
final token = session.get(); // Option<String>
await session.clear().run(); // the one unset; the next get() is None

session.watch().listen((token) => print(token)); // Some on set, None on clear

The value sits under a fixed internal slot, the same one 0.0.x single boxes used, so that data reads in place. LazySingleValueBox is the same surface on the lazy axis. update mirrors Map.update, and getOr(fallback) reads with a default.

πŸ—‚οΈ IterableBox #

A list of values per key. Tags on a post, history for a day.

List ergonomics, and the sharp edge it files off

Hive reads collections back as List<dynamic> no matter what you wrote (issue #150), so a plain Box<List<Person>> opens fine and then throws on the first read after a restart. IterableBox restores the element type at the read boundary and stacks list helpers on top:

final tags = await IterableBox.open<String, int>('post_tags').run();

await tags.put(1, ['flutter', 'dart']).run(); // any Iterable; stored as a private copy
await tags.add(1, 'hive').run(); // append; an absent key becomes [value]
await tags.remove(1, 'dart').run(); // first occurrence; an absent key is a no-op

final postTags = tags.getOr(1); // List<String>: unmodifiable view, empty when absent
final maybe = tags.get(1); // Option<List<String>>: None = absent, Some([]) = stored empty

Worth knowing:

  • Lists you read out are unmodifiable views; lists you put in are copied. Mutating your original afterwards never leaks into the box. add / addAll / remove are read-modify-writes, O(n) in the stored list.
  • Absent isn't the same as empty. get keeps them apart; getOr folds both to [] on purpose.
  • List semantics only: order preserved, duplicates allowed. Sets, maps, and nested collections of custom types stay out (the read-boundary cast can't fix inner reification). Model richer shapes as adapter-registered value types instead.
  • LazyIterableBox is the same surface on the lazy axis.

πŸ”— DualKeyBox #

Two natural dimensions to your data (user + day, row + column). Address it by both parts, query by either.

Lookups, reverse queries, and the codec choice
final events = await DualKeyBox.open<Event, int, int>('user_events').run();

await events.put(userId, dayIndex, event).run();

final one = events.get(userId, dayIndex); // Option<Event>: exact lookups stay O(1)
final usersWeek = events.queryByPrimary(userId); // List<Event>: everything for this user
final everyoneToday = events.queryBySecondary(dayIndex); // List<Event>: everyone, this day

Queries hand back a plain (possibly empty) list, and in 1.0 they're an honest O(K) scan over the live key set: free until you call one, exact when you do, documented instead of hidden. Where a whole key is needed (keys, putAll, watch events) it travels as a (primary, secondary) record. LazyDualKeyBox matches the surface on the lazy axis (queries return Task<List<T>>).

Both parts round-trip through one DualKeyCodec. (int, int) defaults to the safe StringCompositeDualCodec (full-range parts, negatives included, no ceilings). If both parts fit in 16 bits and the numbers matter to you, opt into PackedIntDualCodec (codec: const PackedIntDualCodec()). It packs both parts into a single u32 key and is bit-identical to the old 0.0.x .bitShift scheme, so those boxes read in place. The two codecs trade off measurably; Performance has the head-to-head. Rolling your own part types? Implement DualKeyCodec<K1, K2> and keep the encoding bijective, or reverse queries will lie to you.

πŸŽ›οΈ Make it yours #

The boxes do their work through seams you can swap. Every open (and every lazy constructor) takes these, all optional:

  • codec: a KeyCodec<K> (or DualKeyCodec<K1, K2>) for key types beyond int / String.
  • observer: a BoxObserver to hear semantic events. Silent when you pass nothing.
  • cipher: a hive_ce HiveCipher for an encrypted box.
  • hive pluggables (keyComparator, compactionStrategy, crashRecovery), passed straight through to the engine.
πŸ—οΈ A custom key type

int and String work out of the box. Anything else brings a codec:

final class DateKeyCodec implements KeyCodec<DateTime> {
  const DateKeyCodec();

  @override
  Object encode(DateTime key) => key.toIso8601String();

  @override
  DateTime decode(Object rawKey) => DateTime.parse(rawKey as String);
}

final byDay = await KeyedBox.open<Todo, DateTime>(
  'by_day',
  codec: const DateKeyCodec(),
).run();

encode has to produce an int in 0..0xFFFFFFFF or a String of at most 255 UTF-8 bytes. That's hive's raw key domain, and staying inside it is what keeps your data safe (more on that just below).

πŸ“£ Observing what your code does

Pass an observer and you'll hear every semantic event. Pass none and dispatch costs nothing:

final todos = await KeyedBox.open<Todo, int>(
  'todos',
  observer: const PrintingBoxObserver(), // dart:developer log, DevTools-filterable
).run();

Extend BoxObserver and override only the events you care about; PrintingBoxObserver is the ready-made sink. Two notes on the engine side: hive_ce's own warnings stay on its global logging channel (its logging options filter them), and the Hive Inspector DevTools extension is handy for eyeballing box contents while your observer reports what the code did to them.

πŸ“ Eager or lazy? (measured) #

Folklore says "lazy opens instantly and saves all the memory." The maintainer benchmark (macOS Apple Silicon, AOT, hive_ce 2.19.3) disagrees:

  • Opening costs the same either way, and it scales with file size: ~70 ms at 100K int-keyed entries, ~3 s at 1M. Hive parses every frame to build the keystore on any open. What lazy skips is holding onto the values.
  • Keys always live in RAM, eager or lazy: ~21 to 33 MB at 100K entries, ~200 to 270 MB at 1M (String keys run 20 to 80% heavier than int keys).
  • Reads are where they split. An eager get is ~0.5 to 0.8 Β΅s from memory; a lazy get pays for a disk read at ~22 to 25 Β΅s.

Open cost tracks file size on both axes (the two lines sit right on top of each other), while reads are where they part ways:

Box open time by box size: eager and lazy overlap, both rising with size

Per-read latency by box size on a log scale: eager reads from memory sit far below lazy reads from disk

So reach for eager on hot, value-heavy-read boxes that fit comfortably in RAM, and lazy on value-heavy boxes you read only now and then. Neither opens "instantly" at scale, and keys are a RAM cost you pay regardless.

πŸ›‘οΈ Safer than raw hive, at raw-hive speed #

Release-mode hive_ce takes an out-of-range int key or an oversized String key without complaint, then corrupts quietly: keys wrap into other slots, and an oversized String key can make the whole box file unreadable on its next open. This package rejects exactly those keys with an ArgumentError at the call site, before anything reaches disk.

That safety is measured, not paid for in speed. The wrapper-overhead benchmark (AOT, faΓ§ade vs raw hive_ce) lands the typed surface at roughly 2.5% on eager gets, 1.5% on lazy gets, and 3% on puts. Within noise of raw, with the whole no-null contract sitting on top.

⚑ Performance #

Two dual-key codecs ship, and they trade off like this:

Operation packed int String composite
eager get, 100K entries 51 ms 81 ms
box open (eager), 100K 70 ms 133 ms
putAll, 100K 78 ms 145 ms
full key scan (a query), 100K 5.0 ms 18.9 ms
keystore RSS after open, 100K 33 MB 61 MB
box file size, 100K 1.9 MB 2.6 MB
lazy get / single put codec-indifferent (disk dominates)

Medians from the maintainer benchmark (macOS Apple Silicon, AOT, constant 1-byte values to isolate key cost). Web is unmeasured; its ordering is assumed to follow the VM. StringCompositeDualCodec is the safe default; reach for PackedIntDualCodec when these wins matter and both parts fit in 16 bits.

The table rows are the 100K slice of these curves; the gap widens as boxes grow:

Eager get time by box size: packed-int stays below String composite, the gap widening with scale

Keystore RAM after open by box size: packed-int stays below String composite

πŸ—ΊοΈ Roadmap #

What's on the table for 1.x (and what will never land)

Additive candidates for 1.x, in no committed order:

  • Indexed reverse queries for very large (>100K) datasets: an inverted-index strategy on LazyDualKeyBox, swapping out the O(K) scan behind the same query methods.
  • IsolatedHive support behind the box-acquisition seam (hive_ce itself recommends it for multi-isolate apps), plus BoxCollection wrapping if there's demand.
  • A migration helper API (1.0 documents recipes in MIGRATION.md for now).
  • Sets, maps, and nested collections on the value-codec seam (IterableBox is list-only today).
  • A consumer fakes package (in-memory faΓ§ades for app tests) and Flutter companions (a ValueListenable adapter), as separate packages so the core stays pure Dart.

Deliberately never wrapped, because they fight the typed, codec-addressed key model: auto-increment add / addAll, index-based getAt / putAt / deleteAt / keyAt, toMap, and valuesBetween. Reach for a raw hive_ce box where those genuinely fit.

2
likes
160
points
248
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Type-safe, FP-style abstraction layers for Hive's boxes. Provides Managers for almost all conceivable use-cases and scenarios.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

fpdart, hive_ce, meta

More

Packages that depend on hive_box_manager