Flutter Hive Database Tutorial: Fast, Typed, and Offline‑First

Learn Flutter Hive database from setup to adapters, reactive UI, encryption, migrations, testing, and performance tips—with clear code examples.

ASOasis
6 min read
Flutter Hive Database Tutorial: Fast, Typed, and Offline‑First

Image used for representation purposes only.

Overview

Hive is a lightweight, blazing‑fast NoSQL database written in pure Dart. It’s perfect for Flutter apps that need ultra‑low‑latency local storage, strong typing, and simple persistence without the overhead of SQL. In this tutorial you’ll set up Hive, model data with TypeAdapters, build a reactive UI, secure your boxes with AES encryption, handle migrations, write tests, and apply performance best practices.

When (and When Not) To Use Hive

Use Hive when you need:

  • Offline‑first UX with instant reads/writes
  • Simple key‑value or document‑style storage
  • Strongly typed models (no runtime JSON parsing overhead)
  • Cross‑platform support (Android, iOS, web, desktop)

Consider a relational database if you require complex queries, joins, or strict relational constraints. Hive favors speed and simplicity over SQL‑style querying.

Prerequisites

  • Flutter installed and a recent stable channel
  • Basic Dart/Flutter knowledge

Project Setup

Add the core packages to pubspec.yaml. Version numbers below show major lines—use the latest stable on pub.dev.

dependencies:
  flutter:
    sdk: flutter
  hive: ^2.0.0
  hive_flutter: ^1.0.0
  flutter_secure_storage: ^9.0.0  # for encryption key storage (mobile/desktop)

dev_dependencies:
  build_runner: ^2.0.0
  hive_generator: ^2.0.0
  hive_test: ^1.0.0

Then install:

flutter pub get

Initialize Hive

In Flutter apps, prefer the hive_flutter helper which sets a sensible default storage location for all platforms (including web’s IndexedDB).

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'models/todo.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Hive.initFlutter();

  // Register adapters BEFORE opening boxes
  Hive.registerAdapter(TodoAdapter());

  // Open boxes your app needs at startup
  await Hive.openBox<Todo>('todos');

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Hive Demo',
      theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.amber),
      home: const TodoPage(),
    );
  }
}

Define a Data Model and Generate a TypeAdapter

Hive stores strongly typed objects via adapters. Annotate your model, then generate the adapter with build_runner.

// lib/models/todo.dart
import 'package:hive/hive.dart';
part 'todo.g.dart';

@HiveType(typeId: 0) // unique across your entire app
class Todo extends HiveObject {
  @HiveField(0)
  String title;

  @HiveField(1, defaultValue: false)
  bool done;

  @HiveField(2)
  DateTime? createdAt;

  Todo({required this.title, this.done = false, DateTime? createdAt})
      : createdAt = createdAt ?? DateTime.now();
}

Generate the adapter:

flutter pub run build_runner build --delete-conflicting-outputs

This creates lib/models/todo.g.dart with a TodoAdapter. Ensure you register it before opening boxes.

CRUD Essentials

Hive provides two primary patterns:

  • add(value): auto‑incrementing int keys
  • put(key, value): explicit keys (String, int, etc.)
final todos = Hive.box<Todo>('todos');

// Create
await todos.add(Todo(title: 'Buy milk'));
await todos.put('welcome', Todo(title: 'Welcome task'));

// Read
final first = todos.getAt(0);         // by index
final welcome = todos.get('welcome'); // by key

// Update
if (first != null) {
  first.done = true;
  await first.save();                 // HiveObject helper persists changes
}

await todos.put('welcome', Todo(title: 'Updated title'));

// Delete
await todos.delete('welcome');
await todos.deleteAt(0);

// Bulk
await todos.putAll({
  1: Todo(title: 'Walk dog'),
  2: Todo(title: 'Read a book'),
});

// Housekeeping
await todos.compact(); // reclaims space after many deletions

Tip: Call Hive.isBoxOpen(’todos’) to guard against duplicate opens, and close boxes you don’t need: await todos.close().

Reactive UI with ValueListenableBuilder

Boxes expose a ValueListenable so your widgets rebuild on changes.

// lib/todo_page.dart
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'models/todo.dart';

class TodoPage extends StatelessWidget {
  const TodoPage({super.key});

  @override
  Widget build(BuildContext context) {
    final box = Hive.box<Todo>('todos');

    return Scaffold(
      appBar: AppBar(title: const Text('Hive Todos')),
      body: ValueListenableBuilder(
        valueListenable: box.listenable(),
        builder: (context, Box<Todo> b, _) {
          final items = b.values.toList(growable: false);
          if (items.isEmpty) {
            return const Center(child: Text('No todos yet'));
          }
          return ListView.builder(
            itemCount: items.length,
            itemBuilder: (context, index) {
              final todo = items[index];
              return CheckboxListTile(
                title: Text(todo.title),
                value: todo.done,
                onChanged: (v) async {
                  todo.done = v ?? false;
                  await todo.save();
                },
                secondary: IconButton(
                  icon: const Icon(Icons.delete_outline),
                  onPressed: () => b.deleteAt(index),
                ),
              );
            },
          );
        },
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () async {
          final count = box.length + 1;
          await box.add(Todo(title: 'Task #$count'));
        },
        label: const Text('Add'),
        icon: const Icon(Icons.add),
      ),
    );
  }
}

You can also observe fine‑grained events:

final sub = box.watch().listen((BoxEvent e) {
  // e.key, e.value, e.deleted
});
// Remember to cancel: sub.cancel();

Working with LazyBox for Huge Data

LazyBox defers deserialization until you access items, keeping memory usage low.

final lazy = await Hive.openLazyBox<Todo>('todos_lazy');
await lazy.put('heavy', Todo(title: 'Big payload'));
final fetched = await lazy.get('heavy'); // deserialized only now

Use LazyBox when storing large objects (e.g., blobs, encoded images) or very large collections.

Securing Data with AES Encryption

Hive supports transparent AES‑256 encryption via HiveAesCipher. Never hardcode keys—store them in platform‑secure storage.

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

Future<List<int>> _loadOrCreateKey() async {
  const storage = FlutterSecureStorage();
  const keyName = 'hive_key_v1';

  var encoded = await storage.read(key: keyName);
  if (encoded == null) {
    final key = Hive.generateSecureKey();
    encoded = base64UrlEncode(key);
    await storage.write(key: keyName, value: encoded);
  }
  return base64Url.decode(encoded);
}

Future<Box<Todo>> openEncryptedTodosBox() async {
  final key = await _loadOrCreateKey();
  return Hive.openBox<Todo>(
    'e_todos',
    encryptionCipher: HiveAesCipher(key),
  );
}

Notes:

  • On web, flutter_secure_storage isn’t available; consider a user‑provided passphrase, a server‑derived key, or accept that client‑side storage on web is not high‑security.
  • Rotate keys with care: decrypt with old key, re‑encrypt with new, then persist.

Migrations and Schema Evolution

Keep your TypeAdapter stable:

  • typeId: unique and never changes
  • @HiveField indices: never reuse or reorder
  • Use defaultValue for new fields to remain backward compatible

Example: add a createdAt to older objects after upgrading.

Future<void> migrateTodosToV2() async {
  final box = Hive.box<Todo>('todos');
  final version = (await box.get('schema_version')) as int? ?? 1;
  if (version < 2) {
    for (final k in box.keys) {
      final v = box.get(k);
      if (v is Todo && v.createdAt == null) {
        v.createdAt = DateTime.now();
        await v.save();
      }
    }
    await box.put('schema_version', 2);
  }
}

Call migrateTodosToV2() right after opening your box but before building UI.

Testing with hive_test

Use hive_test to spin up an in‑memory file system for fast, isolated tests.

// test/todo_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:hive_test/hive_test.dart';
import 'package:hive/hive.dart';
import 'package:your_app/models/todo.dart';

void main() {
  setUp(() async {
    await setUpTestHive();
    Hive.registerAdapter(TodoAdapter());
  });

  tearDown(() async {
    await tearDownTestHive();
  });

  test('can add and read todos', () async {
    final box = await Hive.openBox<Todo>('todos');
    await box.add(Todo(title: 'Write tests'));
    expect(box.length, 1);
    expect(box.getAt(0)?.title, 'Write tests');
  });
}

Performance Tips

  • Batch writes: use putAll for many items in one go.
  • Prefer add() with auto‑incremented keys for append‑heavy workloads.
  • Use LazyBox for large payloads to avoid memory spikes.
  • Debounce fast‑changing data (e.g., text input) to reduce write frequency.
  • Compact occasionally after mass deletions: await box.compact().
  • Avoid sharing Box instances across isolates; open the needed box inside each isolate.

Common Pitfalls (and Fixes)

  • HiveError: Cannot write, unknown type: MyModel → You forgot Hive.registerAdapter(MyModelAdapter()) before opening/using the box.
  • Adapter collisions → Ensure every @HiveType has a globally unique typeId.
  • Lost data after refactoring → Never change @HiveField indices; add new indices instead.
  • Build errors after model changes → Re‑run build_runner and use –delete-conflicting-outputs.
  • Opening boxes repeatedly → Guard with Hive.isBoxOpen or keep a singleton repository to manage boxes.

Structuring Your App: Repository Example

Abstract box access behind a repository to keep UI clean and testable.

// lib/data/todo_repo.dart
import 'package:hive/hive.dart';
import '../models/todo.dart';

class TodoRepository {
  final Box<Todo> _box;
  TodoRepository(this._box);

  ValueListenable<Box<Todo>> listenable() => _box.listenable();

  Future<void> add(String title) => _box.add(Todo(title: title));
  List<Todo> all() => _box.values.toList(growable: false);
  Future<void> toggleAt(int index) async {
    final t = _box.getAt(index);
    if (t != null) {
      t.done = !t.done;
      await t.save();
    }
  }
  Future<void> removeAt(int index) => _box.deleteAt(index);
}

Web and Desktop Notes

  • Web uses IndexedDB under the hood; Hive.initFlutter() handles it. Encryption works, but key management is your responsibility.
  • Desktop paths are set by hive_flutter; you can override with Hive.init(customPath) for advanced setups.

Wrap‑Up

You’ve wired up Hive end‑to‑end: initialization, adapters, CRUD, reactive UI, lazy loading, encryption, migrations, testing, and performance tuning. This foundation scales from small feature flags to large offline‑first data sets—while keeping your Flutter app fast and your codebase clean.

Related Posts