Hive NoSQL Database: Setup, Boxes, and TypeAdapters
Hive NoSQL Database: Setup, Boxes, and TypeAdapters
Hive is a lightweight, blazing-fast, key-value NoSQL database written in pure Dart. Unlike SQLite, Hive requires no native dependencies, making it an excellent choice for Flutter apps on all platforms including web. It stores data in binary format using boxes — typed containers similar to tables but schema-free. Hive is particularly suited for storing user preferences, cached API responses, and simple structured objects that must survive app restarts.
path_provider so the location is predictable and sandboxed per platform.Adding Hive to Your Project
Add the required packages to pubspec.yaml:
pubspec.yaml dependencies
dependencies:
hive: ^2.2.3
hive_flutter: ^1.1.0
path_provider: ^2.1.2
dev_dependencies:
hive_generator: ^2.0.1
build_runner: ^2.4.9
hive_flutter extends Hive with a Flutter-specific Hive.initFlutter() initialiser that automatically resolves the correct storage path. hive_generator and build_runner are dev-only tools used to auto-generate TypeAdapter code.
Initialising Hive
Call Hive.initFlutter() in main() before runApp(). This must be awaited, so main becomes async:
main.dart — initialising Hive
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'app.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialise Hive with the Flutter documents directory
await Hive.initFlutter();
// Register TypeAdapters BEFORE opening boxes that use them
Hive.registerAdapter(UserSettingsAdapter());
// Open the boxes you need at startup
await Hive.openBox<String>('settings');
await Hive.openBox<UserSettings>('userProfiles');
runApp(const MyApp());
}
Working with Boxes
A box is Hive's storage unit — a persistent map from string keys to typed values. Once a box is open you access it anywhere in your app without re-opening it.
Hive.openBox<T>(name)— opens a strongly-typed box; all values must be of typeT.Hive.openLazyBox<T>(name)— opens a lazy box where values are loaded from disk only when accessed, ideal for large datasets.Hive.box<T>(name)— retrieves an already-opened box synchronously (throws if not open).
CRUD operations on a Box
// Retrieve the already-opened box (no await needed)
final Box<String> settingsBox = Hive.box<String>('settings');
// --- Write ---
await settingsBox.put('theme', 'dark');
await settingsBox.put('language', 'ar');
// --- Read ---
final String? theme = settingsBox.get('theme'); // 'dark'
final String lang = settingsBox.get('language', defaultValue: 'en'); // 'ar'
// --- Update ---
await settingsBox.put('theme', 'light'); // put() is also an upsert
// --- Delete ---
await settingsBox.delete('language');
// --- Iterate all values ---
for (final key in settingsBox.keys) {
print('$key = ${settingsBox.get(key)}');
}
// --- Close when no longer needed (usually on app exit) ---
await settingsBox.close();
close() if you are done with a box and want to reclaim memory, or use Hive.close() at app shutdown.Storing Custom Objects with TypeAdapters
Hive can only serialise primitive types (int, double, bool, String, List, Map, DateTime, Uint8List) natively. To store custom Dart classes you must register a TypeAdapter — a class that tells Hive how to read and write your object in binary.
The recommended approach is to annotate your model with @HiveType and each field with @HiveField, then let build_runner generate the adapter automatically:
Annotated model and generated adapter
import 'package:hive/hive.dart';
// Part directive — build_runner writes the adapter into this file
part 'user_settings.g.dart';
@HiveType(typeId: 0) // typeId must be unique across all registered adapters (0–223)
class UserSettings extends HiveObject {
@HiveField(0)
late String username;
@HiveField(1)
late String themeMode; // 'light' | 'dark' | 'system'
@HiveField(2)
late bool notificationsEnabled;
@HiveField(3)
late List<String> favouriteCourseIds;
UserSettings({
required this.username,
this.themeMode = 'system',
this.notificationsEnabled = true,
this.favouriteCourseIds = const [],
});
}
// Run: flutter pub run build_runner build --delete-conflicting-outputs
// This generates user_settings.g.dart containing UserSettingsAdapter
Extending HiveObject is optional but recommended — it gives each stored object a reference back to its box so you can call object.save() and object.delete() directly.
Generating and Registering the Adapter
Run build_runner once to generate the .g.dart file:
Running build_runner
# One-shot generation
flutter pub run build_runner build --delete-conflicting-outputs
# Watch mode — regenerates on every save
flutter pub run build_runner watch --delete-conflicting-outputs
The generated UserSettingsAdapter must be registered with Hive before the corresponding box is opened. Register all adapters in main() right after Hive.initFlutter().
@HiveField index after data has been written to disk — Hive uses the integer index, not the field name, for binary encoding. Changing an index corrupts existing stored data. You can safely add new fields with new indices, or mark old fields as @HiveField(n) with a null-compatible type to preserve backward compatibility.Lazy Boxes for Large Datasets
A lazy box loads value metadata at startup but defers reading the actual bytes until you request a specific key. Use it when a box may contain thousands of records:
Opening and using a LazyBox
// Open a lazy box — note the await and LazyBox type
final LazyBox<UserSettings> lazyBox =
await Hive.openLazyBox<UserSettings>('allUsers');
// Reading requires await because it may fetch from disk
final UserSettings? user = await lazyBox.get('user_42');
// Writing is the same as a regular box
await lazyBox.put('user_42', UserSettings(username: 'Edrees'));
Summary
Hive is a powerful, zero-dependency NoSQL store for Flutter. The core workflow is: initialise → register adapters → open boxes → read/write. Primitives work out of the box; custom classes need an annotated model plus a build_runner–generated TypeAdapter. Use regular boxes for small, frequently-accessed data and lazy boxes for large collections. Always register adapters before opening the boxes that use them, and never reassign existing @HiveField indices.
@HiveType(typeId: N) on the class and @HiveField(N) on each field are all you need to write — build_runner produces the full binary serialiser automatically. Keep a registry of your typeId values to avoid collisions as your app grows.