Building a ToDo App with Hive in Flutter
Building a ToDo application is a classic way to learn a new framework or database, and Flutter with Hive offers an excellent combination for creating a fast, reliable, and user-friendly local data storage solution. Hive is a lightweight and blazing-fast key-value database written purely in Dart, making it a perfect choice for Flutter applications that require local persistence without the overhead of more complex databases like SQLite.
Why Hive for a ToDo App?
Hive stands out for several reasons, especially for a simple application like a ToDo list:
- Speed: It's incredibly fast, often outperforming other local databases.
- Simplicity: Its API is straightforward, making it easy to learn and implement.
- Lightweight: No native dependencies, making your app smaller and faster to compile.
- Type Safety: Supports custom objects through
TypeAdapters, ensuring type-safe data storage. - Reactive: Integrates seamlessly with Flutter's reactive paradigms using
ValueListenableBuilderfor real-time UI updates.
Project Setup
First, create a new Flutter project:
flutter create hive_todo_app
cd hive_todo_app
Next, add the necessary dependencies to your pubspec.yaml file. We'll need hive for the core database, hive_flutter for Flutter-specific utilities (like ValueListenableBuilder), path_provider to get the application's document directory, and hive_generator and build_runner for generating type adapters.
dependencies:
flutter:
sdk: flutter
hive: ^2.0.6
hive_flutter: ^1.1.0
path_provider: ^2.0.11
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
hive_generator: ^1.1.3
build_runner: ^2.1.11
After updating pubspec.yaml, run flutter pub get to fetch the dependencies.
flutter pub get
Defining the Data Model
For our ToDo app, each ToDo item will have a title and a boolean indicating if it's completed. We'll create a class TodoItem and annotate it with @HiveType and @HiveField. This tells Hive how to store and retrieve our custom object.
Create a new file lib/models/todo_item.dart:
import 'package:hive/hive.dart';
part 'todo_item.g.dart'; // This file will be generated by hive_generator
@HiveType(typeId: 0) // Unique ID for this type adapter, must be 0-223
class TodoItem extends HiveObject {
@HiveField(0) // Unique ID for each field within this type
String title;
@HiveField(1)
bool isCompleted;
TodoItem({required this.title, this.isCompleted = false});
}
After defining the model, generate the type adapter by running:
flutter pub run build_runner build
This command will create a file named todo_item.g.dart in the same directory as todo_item.dart. This file contains the necessary code for Hive to serialize and deserialize your TodoItem objects.
Initializing Hive and Opening a Box
Before using Hive, it needs to be initialized. This is typically done in the main function of your app. We'll also register our TodoItemAdapter and open a "box" (Hive's term for a database table).
Update your lib/main.dart:
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:hive_todo_app/models/todo_item.dart';
import 'package:hive_todo_app/home_page.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized(); // Required for Flutter services
final appDocumentDirectory = await getApplicationDocumentsDirectory();
await Hive.initFlutter(appDocumentDirectory.path);
// Register the generated adapter
Hive.registerAdapter(TodoItemAdapter());
// Open the box for our TodoItem objects
await Hive.openBox('todoBox');
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Hive Todo App',
theme: ThemeData(
primarySwatch: Colors.blue,
appBarTheme: const AppBarTheme(
centerTitle: true,
),
),
home: const HomePage(),
);
}
}
In the above code:
WidgetsFlutterBinding.ensureInitialized()ensures that Flutter's widget binding is initialized before Hive.getApplicationDocumentsDirectory()gets a path to the directory where the app can store data.Hive.initFlutter()initializes Hive with the given path.Hive.registerAdapter(TodoItemAdapter())tells Hive how to work with ourTodoItemclass.Hive.openBoxopens a box named 'todoBox' specifically for('todoBox') TodoItemobjects. This box will hold all our ToDo items.
Building the User Interface and CRUD Operations
Now, let's create the UI for our ToDo list and implement the Create, Read, Update, and Delete (CRUD) operations. We'll use a ValueListenableBuilder to automatically rebuild the UI whenever data in our 'todoBox' changes.
Create a new file lib/home_page.dart:
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hive_todo_app/models/todo_item.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State createState() => _HomePageState();
}
class _HomePageState extends State {
final TextEditingController _taskController = TextEditingController();
late Box _todoBox;
@override
void initState() {
super.initState();
_todoBox = Hive.box('todoBox'); // Get the opened box
}
@override
void dispose() {
_taskController.dispose();
super.dispose();
}
// --- CRUD Operations ---
// Create (Add) a new TodoItem
void _addTask() {
if (_taskController.text.isNotEmpty) {
_todoBox.add(TodoItem(title: _taskController.text));
_taskController.text = ''; // Clear the input field
Navigator.of(context).pop(); // Close the dialog
}
}
// Update a TodoItem (e.g., toggle completion status)
void _updateTask(TodoItem item, bool isCompleted) {
item.isCompleted = isCompleted;
item.save(); // Calling save() on a HiveObject persists changes
}
// Delete a TodoItem
void _deleteTask(TodoItem item) {
item.delete(); // Calling delete() on a HiveObject removes it from the box
}
// --- UI Helpers ---
void _showAddTaskDialog() {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Add New Todo'),
content: TextField(
controller: _taskController,
decoration: const InputDecoration(hintText: 'Enter todo item'),
autofocus: true,
onSubmitted: (_) => _addTask(), // Add on enter key
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: _addTask,
child: const Text('Add'),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Hive Todo App'),
),
body: ValueListenableBuilder(
valueListenable: _todoBox.listenable(), // Listen for changes in the box
builder: (context, Box box, _) {
if (box.values.isEmpty) {
return const Center(
child: Text('No todos yet! Add one using the + button.'),
);
}
return ListView.builder(
itemCount: box.values.length,
itemBuilder: (context, index) {
final TodoItem todo = box.getAt(index)!; // Get item by index
return Card(
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
elevation: 2,
child: ListTile(
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isCompleted
? TextDecoration.lineThrough
: TextDecoration.none,
color: todo.isCompleted ? Colors.grey[600] : Colors.black87,
),
),
leading: Checkbox(
value: todo.isCompleted,
onChanged: (bool? value) {
if (value != null) {
_updateTask(todo, value);
}
},
),
trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.redAccent),
onPressed: () => _deleteTask(todo),
),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _showAddTaskDialog,
child: const Icon(Icons.add),
),
);
}
}
In HomePage:
_todoBox = Hive.boxretrieves the opened box.('todoBox'); _addTask()uses_todoBox.add()to insert a newTodoItem. When you add aHiveObjectwithout specifying a key, Hive automatically assigns one._updateTask()modifies theisCompletedproperty of aTodoItemand then callsitem.save(). This method, available onHiveObject, persists the changes back to the box._deleteTask()callsitem.delete(), also available onHiveObject, to remove the item from the box.ValueListenableBuilder(valueListenable: _todoBox.listenable(), ...)is the core of reactive UI. It rebuilds its child whenever_todoBoxchanges (e.g., an item is added, updated, or deleted).- Inside the
ValueListenableBuilder, we use aListView.builderto display all items frombox.values.
Running the Application
You can now run your ToDo application:
flutter run
You should see a simple ToDo list. You can add new tasks, mark them as complete, and delete them, with all changes persisting locally using Hive.
Conclusion
Building a ToDo app with Flutter and Hive demonstrates the power and simplicity of this combination for local data persistence. Hive offers a highly performant, easy-to-use, and reactive solution that integrates beautifully with Flutter's widget tree through ValueListenableBuilder. This approach is ideal for many Flutter applications that require offline capabilities or efficient local storage without the complexity of a full-fledged relational database. With Hive, managing your application's data becomes a delightful and straightforward experience.