local_first_hive_storage
A fast, schema-less storage adapter for the local_first ecosystem. Built on Hive's lightning-fast key-value storage with support for namespaces, reactive queries, and metadata management.
Note: This is a companion package to
local_first. You need to install the core package first.
Why local_first_hive_storage?
- Blazing Fast: Hive's pure Dart implementation is optimized for mobile performance
- Schema-less: No column definitions needed - store your model maps directly
- Zero Configuration: Works out of the box with sensible defaults
- Reactive: Built-in
watchQueryfor real-time UI updates - Namespace Support: Multi-user isolation with
useNamespace - Metadata Storage: Persistent config values via
setConfigValue/getConfigValue - Lazy Loading: Optional lazy boxes for memory-efficient large datasets
Features
- ✅ Pure Dart: No native dependencies, works on all Flutter platforms
- ✅ Key-Value Storage: Fast Hive boxes for each repository
- ✅ Schema-less: Store JSON maps without defining schemas
- ✅ Namespaces: Isolate data per user or tenant
- ✅ Reactive Queries:
watchQuerywith real-time updates - ✅ Metadata Table: Store app configuration and sync state
- ✅ Lazy Collections: Reduce memory usage for large datasets
- ✅ CRUD Operations: Full support for create, read, update, delete
- ✅ Query Filtering: In-memory filtering after load
Installation
Add the core package and Hive adapter to your pubspec.yaml:
dependencies:
local_first: ^0.6.0
local_first_hive_storage: ^0.2.0
Then install it with:
flutter pub get
Quick Start
import 'package:local_first/local_first.dart';
import 'package:local_first_hive_storage/local_first_hive_storage.dart';
// 1) Define your model (keep it immutable)
class Todo {
const Todo({
required this.id,
required this.title,
required this.updatedAt,
});
final String id;
final String title;
final DateTime updatedAt;
JsonMap toJson() => {
'id': id,
'title': title,
'updated_at': updatedAt.toUtc().toIso8601String(),
};
factory Todo.fromJson(JsonMap json) => Todo(
id: json['id'] as String,
title: json['title'] as String,
updatedAt: DateTime.parse(json['updated_at']).toUtc(),
);
static Todo resolveConflict(Todo local, Todo remote) =>
local.updatedAt.isAfter(remote.updatedAt) ? local : remote;
}
// 2) Create repository
final todoRepository = LocalFirstRepository<Todo>.create(
name: 'todo',
getId: (todo) => todo.id,
toJson: (todo) => todo.toJson(),
fromJson: Todo.fromJson,
onConflict: Todo.resolveConflict,
);
// 3) Initialize client with Hive storage
Future<void> main() async {
final client = LocalFirstClient(
repositories: [todoRepository],
localStorage: HiveLocalFirstStorage(),
syncStrategies: [
// Add your sync strategy here
],
);
await client.initialize();
// 4) Use the repository
await todoRepository.upsert(
Todo(
id: '1',
title: 'Buy milk',
updatedAt: DateTime.now().toUtc(),
),
needSync: true,
);
// 5) Query data
final todos = await todoRepository.getAll();
print('Todos: ${todos.length}');
}
Architecture
Storage Structure
┌────────────────────────────────────────────┐
│ HiveLocalFirstStorage │
│ ┌──────────────────────────────────────┐ │
│ │ Metadata Box (__config__) │ │
│ │ - Sync sequences │ │
│ │ - App configuration │ │
│ │ - Namespace state │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Repository Boxes │ │
│ │ ┌────────────────────────────────┐ │ │
│ │ │ todo_box │ │ │
│ │ │ key: eventId → JsonMap │ │ │
│ │ └────────────────────────────────┘ │ │
│ │ ┌────────────────────────────────┐ │ │
│ │ │ user_box │ │ │
│ │ │ key: eventId → JsonMap │ │ │
│ │ └────────────────────────────────┘ │ │
│ └──────────────────────────────────────┘ │
└────────────────────────────────────────────┘
Data Flow
┌─────────────────────────────────────────┐
│ Application Code │
│ todoRepository.upsert(todo) │
└────────────────┬────────────────────────┘
│
┌────────────────▼────────────────────────┐
│ LocalFirstClient │
│ - Creates LocalFirstEvent │
│ - Wraps data with metadata │
└────────────────┬────────────────────────┘
│
┌────────────────▼────────────────────────┐
│ HiveLocalFirstStorage │
│ - Serializes to JsonMap │
│ - Stores in Hive box │
└────────────────┬────────────────────────┘
│
┌────────────────▼────────────────────────┐
│ Hive Box (Disk) │
│ eventId: {id, operation, data, ...} │
└─────────────────────────────────────────┘
Configuration Options
Lazy Collections
Enable lazy boxes to reduce memory usage for large datasets:
final storage = HiveLocalFirstStorage(
lazyCollections: true, // Default: false
);
When to use:
- ✅ Large datasets (>10,000 records per repository)
- ✅ Memory-constrained devices
- ✅ Repositories with infrequent access
When NOT to use:
- ❌ Small datasets (<1,000 records)
- ❌ Frequently accessed repositories
- ❌ Real-time reactive queries (performance impact)
Namespace Support
Isolate data per user or tenant:
final storage = HiveLocalFirstStorage();
await storage.initialize();
// Switch to user Alice's namespace
await storage.useNamespace('user_alice');
// All operations now affect Alice's data
await todoRepository.upsert(todo);
// Switch to user Bob's namespace
await storage.useNamespace('user_bob');
// Now operating on Bob's data
final bobTodos = await todoRepository.getAll();
Use cases:
- 👤 Multi-user applications
- 🏢 Multi-tenant apps
- 📱 Multiple accounts support
- 🔐 Data isolation requirements
Supported Config Types
The metadata storage supports these types via setConfigValue/getConfigValue:
| Type | Example | Use Case |
|---|---|---|
bool |
true |
Feature flags, preferences |
int |
42 |
Counters, sync sequences |
double |
3.14 |
Ratings, calculations |
String |
'hello' |
User names, tokens |
List<String> |
['a', 'b'] |
Tags, categories |
Example: Storing Metadata
final client = LocalFirstClient(
repositories: [todoRepository],
localStorage: HiveLocalFirstStorage(),
syncStrategies: [],
);
await client.initialize();
// Store sync sequence
await client.setConfigValue('last_sync_seq', 42);
// Store user preference
await client.setConfigValue('dark_mode', true);
// Store feature flags
await client.setConfigValue('enabled_features', ['chat', 'notifications']);
// Retrieve values
final lastSeq = await client.getConfigValue<int>('last_sync_seq');
final darkMode = await client.getConfigValue<bool>('dark_mode');
final features = await client.getConfigValue<List<String>>('enabled_features');
Reactive Queries
Watch for real-time updates:
// Watch all todos
final stream = todoRepository.watchQuery();
stream.listen((todos) {
print('Todos updated: ${todos.length}');
});
// In Flutter
StreamBuilder<List<LocalFirstEvent<Todo>>>(
stream: todoRepository.watchQuery(),
builder: (context, snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
final events = snapshot.data!;
final todos = events.map((e) => e.data).toList();
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) => TodoTile(todos[index]),
);
},
)
Comparison with SQLite Storage
| Feature | HiveLocalFirstStorage | SqliteLocalFirstStorage |
|---|---|---|
| Performance | Faster (pure Dart) | Fast (native SQLite) |
| Schema | Schema-less | Schema-based |
| Query Capabilities | In-memory filtering | Rich SQL queries |
| Indexes | No indexes | Column indexes |
| Storage Size | Smaller | Larger (with indexes) |
| Setup Complexity | Zero config | Define schemas |
| Best For | Simple models, speed | Complex queries, filtering |
| Platform Support | All platforms | All platforms |
| Memory Usage | Low (with lazy) | Very low |
Choose Hive when:
- ✅ You want the fastest performance
- ✅ Your models are simple and don't need complex filtering
- ✅ You prefer zero configuration
- ✅ You're building for mobile/web and want pure Dart
Choose SQLite when:
- ✅ You need complex SQL queries
- ✅ You want indexed filtering and sorting
- ✅ Your data has relational aspects
- ✅ You need advanced query capabilities
CRUD Operations
Create/Update (Upsert)
await todoRepository.upsert(
Todo(id: '1', title: 'Buy milk', updatedAt: DateTime.now()),
needSync: true, // Mark for synchronization
);
Read Single Item
final event = await todoRepository.getById('1');
if (event != null) {
print('Todo: ${event.data.title}');
}
Read All Items
final events = await todoRepository.getAll();
final todos = events.map((e) => e.data).toList();
Delete
await todoRepository.delete('1', needSync: true);
Query with Filter
// Note: Hive loads all data then filters in memory
final events = await todoRepository.query();
final incompleteTodos = events
.map((e) => e.data)
.where((todo) => !todo.completed)
.toList();
Best Practices
1. Use Lazy Boxes for Large Datasets
// For repositories with >10k records
final storage = HiveLocalFirstStorage(lazyCollections: true);
2. Keep Models Immutable
class Todo {
const Todo({required this.id, required this.title}); // Immutable
final String id;
final String title;
// Use copyWith for updates
Todo copyWith({String? title}) => Todo(
id: id,
title: title ?? this.title,
);
}
3. Handle Conflicts Properly
static Todo resolveConflict(Todo local, Todo remote) {
// Last-write-wins based on timestamp
return local.updatedAt.isAfter(remote.updatedAt) ? local : remote;
// Or merge fields
// return Todo(
// id: local.id,
// title: remote.title, // Take remote title
// completed: local.completed, // Keep local completion status
// );
}
4. Use Namespaces for Multi-User Apps
// On login
await storage.useNamespace('user_${userId}');
// On logout
await storage.useNamespace(null); // Clear namespace
5. Store Metadata for Sync State
// Save last sync sequence
await client.setConfigValue('__last_seq__$repositoryName', sequence);
// Load on next sync
final lastSeq = await client.getConfigValue<int>('__last_seq__$repositoryName');
Troubleshooting
Data Not Persisting
Symptoms: Data disappears after app restart
Solutions:
- Ensure you await
client.initialize():await client.initialize(); // Don't forget await! - Check that Hive boxes are being opened:
// Enable Hive logging Hive.init(path); // Ensure path is correct
Performance Issues with Large Datasets
Symptoms: Slow queries or high memory usage
Solutions:
- Enable lazy collections:
HiveLocalFirstStorage(lazyCollections: true) - Implement pagination at application level:
final page1 = todos.skip(0).take(20).toList(); final page2 = todos.skip(20).take(20).toList();
Namespace Data Isolation Issues
Symptoms: Data from different users mixing
Solutions:
- Always call
useNamespacebefore operations:await storage.useNamespace('user_$userId'); await todoRepository.getAll(); // Now isolated - Verify namespace is set:
print('Current namespace: ${storage.currentNamespace}');
Box Already Open Errors
Symptoms: HiveError: Box is already open
Solutions:
- Don't manually open Hive boxes - let the adapter handle it
- Only create one
LocalFirstClientinstance:// Good: Singleton static final client = LocalFirstClient(...); // Bad: Multiple instances final client1 = LocalFirstClient(...); final client2 = LocalFirstClient(...); // ❌ Don't do this
watchQuery Not Updating
Symptoms: UI not reflecting changes
Solutions:
- Ensure you're using
needSync: true:await todoRepository.upsert(todo, needSync: true); - Check that StreamBuilder is properly set up:
StreamBuilder<List<LocalFirstEvent<Todo>>>( stream: todoRepository.watchQuery(), // Correct // NOT: stream: todoRepository.query(), // ❌ Wrong
Testing
Unit Tests
Run tests from this package root:
flutter test
Integration Tests
flutter test integration_test/
Mock Storage for Tests
// Use in-memory storage for tests
final testClient = LocalFirstClient(
repositories: [todoRepository],
localStorage: InMemoryLocalFirstStorage(), // No disk I/O
syncStrategies: [],
);
Example App
This package includes a complete example demonstrating:
- Hive storage setup
- REST API sync with PeriodicSyncStrategy
- Multi-repository support
- Namespace isolation
- Reactive UI updates
To run the example:
cd local_first_hive_storage/example
flutter pub get
flutter run
See the example README for detailed setup instructions including server configuration.
Contributing
Contributions are welcome. See CONTRIBUTING.md for guidelines.
Support the Project 💰
Your contributions help us enhance and maintain our plugins. Donations are used to procure devices and equipment for testing compatibility across platforms and versions.
License
This project is available under the MIT License. See LICENSE for details.

