Local Data Storage

Hive NoSQL Database: Setup, Boxes, and TypeAdapters

16 min Lesson 8 of 12

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.

Note: Hive stores all data in the app's documents directory by default. On Flutter, you typically initialize it with the path from 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 type T.
  • 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();
Tip: You rarely need to close boxes manually. Hive flushes data to disk automatically on each write. Only call 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().

Warning: Never change a @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.

Key Takeaway: @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.