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.
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
Flutter Flame Tutorial: Build a 2D “Star Collector” Game
Build a complete 2D game with Flutter Flame: setup, sprites, input, collisions, HUD, audio, performance tips, and multiplatform builds.
Flutter + Shelf + Local Storage: A Practical, Offline‑First Tutorial
Build a Flutter app with a Shelf backend: local JSON storage on the server, Hive caching on the client, and offline-first sync.
Flutter Desktop Tutorial: Build and Ship Apps for Windows and macOS
Step-by-step Flutter desktop tutorial for Windows and macOS: setup, coding patterns, plugins, packaging, signing, and CI tips to ship production apps.