Building Offline-First Apps with Flutter and Hive
Building Offline-First Apps with Flutter and Hive
Offline-first is a design philosophy: your app works fully without a network connection, and syncs changes when connectivity is restored. It is not just about caching — it is about designing your data layer so that the local store is the source of truth, and the network is a sync mechanism rather than a dependency. Flutter and Hive make this achievable without enormous complexity.
Why Offline-First Matters
Users live in the real world — tunnels, flights, basements, rural areas. An app that degrades gracefully (or does not degrade at all) under poor connectivity earns trust. Beyond reliability, local data access is dramatically faster than network calls, making your app feel snappier even when a connection is available.
What Is Hive?
Hive is a lightweight, key-value database written in pure Dart. It stores data in binary format using MessagePack encoding and runs on iOS, Android, Web, and desktop without native dependencies. For structured data, Hive supports typed adapters that serialize Dart objects.
Alternatives include sqflite (SQLite), drift (typed SQLite ORM), and isar (Hive's successor from the same author). For most offline-first use cases, Hive strikes the right balance of simplicity and performance.
Setting Up Hive
dependencies:
hive: ^2.2.3
hive_flutter: ^1.1.0
dev_dependencies:
hive_generator: ^2.0.1
build_runner: ^2.4.0
Initialize in main.dart:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
runApp(const MyApp());
}
Defining a Type Adapter
For structured data, define a model with @HiveType and generate the adapter:
import 'package:hive/hive.dart';
part 'task.g.dart';
@HiveType(typeId: 0)
class Task extends HiveObject {
@HiveField(0)
late String id;
@HiveField(1)
late String title;
@HiveField(2)
late bool completed;
@HiveField(3)
late DateTime createdAt;
}
Generate the adapter:
flutter pub run build_runner build
Register the adapter before opening the box:
Hive.registerAdapter(TaskAdapter());
final box = await Hive.openBox<Task>('tasks');
Basic CRUD Operations
// Create
await box.put(task.id, task);
// Read
final task = box.get('task_id_123');
// Read all
final allTasks = box.values.toList();
// Update (since Task extends HiveObject)
task.completed = true;
await task.save();
// Delete
await box.delete('task_id_123');
The Sync Architecture
The core offline-first pattern:
- Write locally first — every user action writes to Hive immediately
- Queue sync operations — track pending changes (creates, updates, deletes)
- Sync in the background — when online, replay the queue against your server
- Resolve conflicts — last-write-wins is simplest; more complex apps need CRDTs or timestamps
class SyncQueue {
final Box<SyncOperation> _queue = Hive.box('sync_queue');
Future<void> enqueue(SyncOperation op) async {
await _queue.add(op);
}
Future<void> flush(ApiService api) async {
for (final op in _queue.values.toList()) {
try {
await api.execute(op);
await op.delete();
} catch (e) {
// Leave in queue; retry next flush
break;
}
}
}
}
Connectivity Detection
Use the connectivity_plus package to trigger syncs when connectivity changes:
connectivity.onConnectivityChanged.listen((result) {
if (result != ConnectivityResult.none) {
syncQueue.flush(api);
}
});
Watching Hive for UI Updates
Hive supports reactive streams that work naturally with Flutter's StreamBuilder or with Riverpod's StreamProvider:
StreamBuilder<BoxEvent>(
stream: box.watch(),
builder: (context, snapshot) {
final tasks = box.values.toList();
return TaskList(tasks: tasks);
},
)
Data Migration
As your schema evolves, handle migrations in the adapter. Hive's typeId and HiveField indices must remain stable across versions. Adding new fields with new index numbers is backward compatible; changing existing indices breaks existing data.
Conclusion
Flutter and Hive together make offline-first development accessible. The key insight is to treat the local database as the primary store and the remote API as a sync target. Users get instant responsiveness, the app works in any connectivity condition, and sync happens transparently in the background. This architecture is more complex than simple API calls, but the reliability and perceived performance improvements are worth the investment for any app where data matters.
Sign in to like, dislike, or report.