Flutter Hive Tutorial: Fast Local NoSQL Storage

Coding Liquids blog cover featuring Sagnik Bhattacharya for Flutter Hive Tutorial: Fast Local NoSQL Storage, with hive_flutter initialisation and boxes, put, get, delete, and default values, and typeadapter with hivetype and hivefield plus build_runner.
Coding Liquids blog cover featuring Sagnik Bhattacharya for Flutter Hive Tutorial: Fast Local NoSQL Storage, with hive_flutter initialisation and boxes, put, get, delete, and default values, and typeadapter with hivetype and hivefield plus build_runner.

The adapter-generation sequence is clearest when the build command and registration step run on screen.

Subscribe on YouTube@codingliquids

Know where Hive fits

Hive is a pure-Dart, box-based NoSQL store suited to local objects addressed by keys. It avoids SQL mapping for simple caches, settings collections, and small offline-first models.

Complex joins, ad-hoc reporting, or strong relational constraints favour SQLite or Drift.

Initialise Hive and open named boxes

Call WidgetsFlutterBinding.ensureInitialized, await Hive.initFlutter, register adapters, then await Hive.openBox. Open each box once and pass it to the widgets or repositories that own its data.

Reading from a box before it opens throws, while opening the same box repeatedly complicates ownership.

flutter pub add hive hive_flutter flutter_secure_storage
flutter pub add --dev build_runner hive_generator

An Instagram carousel explains why Hive field numbers and adapter IDs must remain stable after release.

Follow me on Instagram@sagnikteaches

Store primitive values with explicit defaults

put writes a key, get reads it, and delete removes it; primitive values need no generated adapter. Supply defaultValue for keys that may not exist after first install or a reset.

Assuming get returns a non-null bool turns a missing key into a runtime cast failure.

The Complete Flutter Guide course thumbnail

Build production-ready Hive Tutorial features

The Complete Flutter Guide turns hive tutorial into maintainable app architecture, polished UI, and testable production code.

Enrol now

Keep Hive runtime libraries above the divider and its adapter generator strictly under dev_dependencies.

dependencies:
  flutter:
    sdk: flutter
  hive: any
  hive_flutter: any
  flutter_secure_storage: any

dev_dependencies:
  build_runner: any
  hive_generator: any

Generate and register a TodoAdapter correctly

Annotate the model with HiveType and fields with stable HiveField numbers, add part todo.g.dart, and run build_runner. Place hive_generator and build_runner under dev_dependencies, import neither in app code, and register TodoAdapter before opening its box.

Reusing a field number for a different property corrupts compatibility with previously stored objects.

import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';

part 'todo.g.dart';

@HiveType(typeId: 1)
class Todo extends HiveObject {
  Todo({required this.title, this.done = false});
  @HiveField(0)
  String title;
  @HiveField(1)
  bool done;
}

Rebuild from box.listenable changes

hive_flutter adds box.listenable so ValueListenableBuilder can rebuild when specified keys change. Read the current values from the box argument supplied to the builder and keep the rebuilt subtree small.

Calling setState as well as relying on the box listenable causes redundant frames.

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Hive.initFlutter();
  Hive.registerAdapter(TodoAdapter());
  await Hive.openBox<Todo>('todos');
  runApp(const MyApp());
}

LinkedIn readers can find a deeper comparison of box boundaries, lazy reads, and recoverable cache data.

Connect on LinkedInSagnik Bhattacharya
The Complete Flutter Guide course thumbnail

The Complete Flutter Guide: Build Android, iOS and Web apps

Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.

Enrol now

Open a LazyBox for large collections

LazyBox keeps keys in memory but reads values asynchronously from disk on demand. Use it when eager deserialisation of a large box would hurt startup memory or latency.

A LazyBox get returns a Future, so code written for an ordinary Box cannot be copied unchanged.

ValueListenableBuilder<Box<Todo>>(
  valueListenable: Hive.box<Todo>('todos').listenable(),
  builder: (context, box, _) => ListView.builder(
    itemCount: box.length,
    itemBuilder: (_, index) {
      final todo = box.getAt(index)!;
      return CheckboxListTile(
        value: todo.done,
        title: Text(todo.title),
        onChanged: (value) { todo.done = value ?? false; todo.save(); },
      );
    },
  ),
)

Encrypt a box with a protected key

HiveAesCipher accepts a 256-bit key when the box is opened, while flutter_secure_storage can protect that key. Generate the key once, store its base64 form securely, and reuse exactly the same bytes for later opens.

Saving the encryption key beside the Hive file defeats encryption, and losing it makes the box unreadable.

import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

final secure = FlutterSecureStorage();
var encodedKey = await secure.read(key: 'hive_key');
if (encodedKey == null) {
  encodedKey = base64UrlEncode(Hive.generateSecureKey());
  await secure.write(key: 'hive_key', value: encodedKey);
}
final cipher = HiveAesCipher(base64Url.decode(encodedKey));
final secrets = await Hive.openBox<String>('secrets', encryptionCipher: cipher);

Keep the box format evolvable

Hive field numbers are durable identifiers, not positions in the current Dart class. Once a HiveField number has shipped, leave it assigned even if the property is removed; reusing it can decode old bytes into the wrong meaning. New fields should be nullable or have a safe default so records written by an earlier release still open. Commit generated adapters and regenerate them whenever the annotated model changes, then review the generated field map as part of the diff.

Choose box boundaries around ownership and access patterns. A small settings box and a larger cache box can have different clearing policies, while one enormous box makes account removal and migrations harder to reason about. Open required boxes before the dependent screen appears, but avoid delaying the whole app for data that belongs to a later route. Lazy boxes reduce memory use for large values at the cost of asynchronous reads, so the repository interface should make that distinction visible.

Updates involving several keys need an application-level recovery plan because a sequence of writes can be interrupted. Store a complete immutable record when possible, or use a pending marker that lets startup finish or roll back the operation. Avoid persisting a mutable object and changing it without calling save; the in-memory value may look correct until restart. Encryption protects box contents at rest only when the key itself is kept outside the box in secure platform storage.

Upgrade tests should open a fixture produced with the previous adapter, read every model, write it again, and reopen the box. Add cases for deleted optional fields, unknown enum values, and corrupted records that must not blank the entire screen. Production diagnostics can record the box name, adapter type ID, and failing operation, but not serialised user values. If a cache can be recreated, catching the read failure and rebuilding it is reasonable; irreplaceable user-created data deserves export, backup, and a carefully versioned migration instead.

Type adapter IDs also form a global registry within the application. Reserve them centrally, never assign one ID to two model classes, and register each adapter before opening a box that contains its values. A “Cannot write, unknown type” failure points to missing registration, while a type-ID collision can decode data with the wrong adapter and is more dangerous. Isolate tests with unique temporary directories and close or delete boxes between cases so cached box instances cannot leak state. For account switching, name boxes by a safe internal account identifier or clear them completely before the next session reads.

Common mistakes

  • Know where Hive fits: In Hive boxes, complex joins, ad-hoc reporting, or strong relational constraints favour SQLite or Drift; inspect this Hive boxes cause before changing another Hive boxes widget.
  • Initialise Hive and open named boxes: In Hive boxes, reading from a box before it opens throws, while opening the same box repeatedly complicates ownership; inspect this Hive boxes cause before changing another Hive boxes widget.
  • Store primitive values with explicit defaults: In Hive boxes, assuming get returns a non-null bool turns a missing key into a runtime cast failure; inspect this Hive boxes cause before changing another Hive boxes widget.
  • Generate and register a TodoAdapter correctly: In Hive boxes, reusing a field number for a different property corrupts compatibility with previously stored objects; inspect this Hive boxes cause before changing another Hive boxes widget.
  • Rebuild from box.listenable changes: In Hive boxes, calling setState as well as relying on the box listenable causes redundant frames; inspect this Hive boxes cause before changing another Hive boxes widget.
  • Open a LazyBox for large collections: In Hive boxes, a LazyBox get returns a Future, so code written for an ordinary Box cannot be copied unchanged; inspect this Hive boxes cause before changing another Hive boxes widget.

Frequently asked questions

How does know where Hive fits work in Hive boxes?

For Hive boxes, the starting rule is that hive is a pure-Dart, box-based NoSQL store suited to local objects addressed by keys. Apply this Hive boxes rule first because know where Hive fits determines whether the Hive boxes pattern fits.

Why does store primitive values with explicit defaults matter for Hive boxes?

In Hive boxes, supply defaultValue for keys that may not exist after first install or a reset. Keeping store primitive values with explicit defaults at the Hive boxes call site exposes the Hive boxes return value directly.

What failure should I test first in Hive boxes?

First reproduce the Hive boxes case where calling setState as well as relying on the box listenable causes redundant frames. The corrected widget relies on the box listenable alone and produces one rebuild for the observed change.

How can I verify Hive boxes before release?

Exercise encrypt a box with a protected key with real Hive boxes inputs on every shipped platform. Inspect the final Hive boxes callback or output; a successful Hive boxes button tap alone is not proof.

Further reads

Connect Hive boxes to the surrounding Flutter stack through these published tutorials:

Sources: Flutter documentation and the package documentation on pub.dev; Hive boxes examples verified against current stable Flutter.