Hive in Flutter: A Detailed Guide with Injectable, Freezed, and Cubit in Clean Architecture
Peace upon you …
When building Flutter applications, data persistence is a crucial part of maintaining a smooth user experience. The Hive package is a lightweight and efficient NoSQL database designed specifically for Flutter and Dart. It offers a fast, reliable, and easy-to-use data storage solution that works offline and is ideal for apps with local storage needs.
This article will dive into Hive in detail and guide you through integrating it with Injectable for dependency injection, Freezed for immutable data classes, and Cubit for state management in the context of Clean Architecture.
What is Hive?
Hive is a lightweight key-value database for Flutter. Unlike other databases, Hive doesn’t require any complex setup or SQL queries. It’s a NoSQL solution that allows you to store and retrieve data in a simple way.
Key Features of Hive:
- Cross-platform: Works on Android, iOS, and the web.
- Lightweight: Hive is designed to be small and fast.
- NoSQL: You store data as key-value pairs.
- Offline support: Hive operates fully offline.
- Binary storage: Hive uses binary storage, making it highly efficient.
1. Setting Up Hive in Flutter
Let’s start by setting up Hive in your Flutter project.
Step 1: Add Hive dependencies
First, include the necessary Hive dependencies in your pubspec.yaml
file:
dependencies:
hive: ^2.0.0
hive_flutter: ^1.1.0
hive_generator: ^1.1.0
path_provider: ^2.0.0
freezed_annotation: ^2.1.0
injectable: ^2.0.0
flutter_bloc: ^8.0.1
dev_dependencies:
build_runner: ^2.1.0
freezed: ^2.1.0
Step 2: Initialize Hive
In the main function of your Flutter app, you need to initialize Hive and register adapters (if you use custom types):
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Hive
await Hive.initFlutter();
// Register Hive Adapters (for custom types)??
runApp(MyApp());
}
2. Using Hive with Custom Types
Hive works best with primitive types like int
, String
, List
, and Map
. However, for complex or custom data types, you need to create a TypeAdapter to serialize and deserialize your custom classes.
Let’s say we want to store a User
model in Hive. We’ll use Freezed to create an immutable data class for the User
and generate the necessary Hive adapter.
Step 1: Define the User Model with Freezed
Create a user.dart
file and define your User
class using Freezed annotations:
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive/hive.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
class User with _$User {
@HiveType(typeId: 0) // Hive adapter type ID
const factory User({
@HiveField(0) required String id,
@HiveField(1) required String name,
@HiveField(2) required int age,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
In the above code:
- @freezed: This annotation generates the required boilerplate for the immutable data class.
- @HiveType and @HiveField: These annotations define the model for Hive, where
typeId
is a unique identifier for the type, andHiveField
indicates the position of each field.
Step 2: Generate the User Model and Adapter
Run the following command to generate the Hive adapter and the Freezed class:
dart run build_runner watch --delete-conflicting-outputs
This will generate the required .g.dart
and .freezed.dart
files for your User
class.
Step 3: Register the User Adapter in Hive
In the main.dart
file, register the UserAdapter
with Hive during initialization:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Hive
await Hive.initFlutter();
// Register the User adapter
Hive.registerAdapter(UserAdapter());
runApp(MyApp());
}
3. Integrating Hive in Clean Architecture with Cubit, Injectable, and Freezed
Now that Hive is set up, we can integrate it into a Clean Architecture framework using Cubit for state management and Injectable for dependency injection.
Clean Architecture Structure
We’ll use the following structure:
- Data Layer: Handles data access (Hive in this case).
- Domain Layer: Contains business logic (Use cases).
- Presentation Layer: The UI and state management using Cubit.
Step 1: Setting Up Dependency Injection with Injectable
Create an injection.dart
file to configure the Injectable package:
import 'package:injectable/injectable.dart';
import 'package:get_it/get_it.dart';
import 'injection.config.dart';
final getIt = GetIt.instance;
@InjectableInit()
void configureInjection(String env) => $initGetIt(getIt);
Run this command to generate the injection.config.dart
:
dart run build_runner watch --delete-conflicting-outputs
Now, you can inject Hive services or repositories into any class.
Step 2: Create a Repository for Hive in the Data Layer
In the data layer, create a user_repository.dart
file to handle Hive operations:
import 'package:hive/hive.dart';
import 'package:injectable/injectable.dart';
import '../domain/user.dart';
abstract class IUserRepository {
Future<void> saveUser(User user);
User? getUser(String id);
}
@LazySingleton(as: IUserRepository)
class UserRepository implements IUserRepository {
final Box<User> _userBox;
UserRepository(this._userBox);
@override
Future<void> saveUser(User user) async {
await _userBox.put(user.id, user);
}
@override
User? getUser(String id) {
return _userBox.get(id);
}
}
In this code, we inject the Hive Box
of User
type and use it to store and retrieve users.
Step 3: Create Use Cases in the Domain Layer
Create use cases for saving and retrieving users:
import 'package:injectable/injectable.dart';
import '../entities/user.dart';
import '../repositories/i_user_repository.dart';
@lazySingleton
class SaveUser {
final IUserRepository _repository;
SaveUser(this._repository);
Future<void> call(User user) async {
await _repository.saveUser(user);
}
}
@lazySingleton
class GetUser {
final IUserRepository _repository;
GetUser(this._repository);
User? call(String id) {
return _repository.getUser(id);
}
}
Step 4: Set Up Cubit in the Presentation Layer
Create a Cubit to handle the business logic and UI state:
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import '../domain/user.dart';
import '../use_cases/get_user.dart';
import '../use_cases/save_user.dart';
part 'user_state.dart';
@injectable
class UserCubit extends Cubit<UserState> {
final SaveUser _saveUser;
final GetUser _getUser;
UserCubit(this._saveUser, this._getUser) : super(UserInitial());
Future<void> saveUser(User user) async {
emit(UserLoading());
await _saveUser(user);
emit(UserSaved());
}
Future<void> loadUser(String id) async {
emit(UserLoading());
final user = _getUser(id);
if (user != null) {
emit(UserLoaded(user));
} else {
emit(UserError('User not found'));
}
}
}
The Cubit handles loading and saving users using the GetUser
and SaveUser
use cases.
Step 5: Use Cubit in Your UI
In your UI, you can use BlocBuilder
to interact with the Cubit:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'user_cubit.dart';
import 'user.dart';
class UserPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('User')),
body: BlocBuilder<UserCubit, UserState>(
builder: (context, state) {
state.whenOrNull(
UserLoading:()=>CircularProgressIndicator(),
UserLoaded:()=>Text('User: ${state.user.name}'),
UserLoaded:()=>Text('Error: ${state.message}'),
orElse: () => Center(child: Text('No user data'),
);
}),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Save a user
context.read<UserCubit>().saveUser(User(id: '1', name: 'John', age: 30));
},
child: Icon(Icons.save),
),
);
}
}
Conclusion
By integrating Hive with Injectable, Freezed, and Cubit in a Clean Architecture structure, you can efficiently manage data persistence in your Flutter apps while maintaining a clear separation of concerns. This approach allows you to build scalable, maintainable applications with robust local storage and dependency injection patterns.