Flutter developers have several options for local data storage. Among the most popular are Hive, Drift (formerly Moor), and SharedPreferences. Each solution offers a different set of features, performance characteristics, and use cases. Understanding their strengths and weaknesses will help you choose the right storage option for your Flutter application.
Introduction to Local Data Storage in Flutter
Local data storage is crucial for Flutter applications that need to persist data locally on a user’s device. This can range from simple settings to complex relational data. Here’s an overview of the three options we’ll explore:
- SharedPreferences: A simple solution for storing key-value pairs.
- Hive: A lightweight NoSQL database.
- Drift: A powerful, type-safe SQLite wrapper.
1. SharedPreferences
SharedPreferences is part of the shared_preferences
plugin, providing a straightforward way to store simple data in key-value pairs. It is suitable for basic data like user preferences or application settings.
Pros
- Simplicity: Very easy to use and understand.
- Lightweight: No significant overhead or setup.
- Asynchronous: Non-blocking operations ensure a smooth UI experience.
Cons
- Limited Data Types: Primarily supports simple types (int, bool, double, String, StringList).
- No Data Relationships: Not suitable for complex data structures or relational data.
- No Transactions: No built-in support for atomic transactions.
Usage
Step 1: Add Dependency
Add the shared_preferences
plugin to your pubspec.yaml
:
dependencies:
shared_preferences: ^2.2.2
Step 2: Store and Retrieve Data
import 'package:shared_preferences/shared_preferences.dart';
class SharedPreferencesHelper {
static Future saveString(String key, String value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(key, value);
}
static Future getString(String key) async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(key);
}
static Future remove(String key) async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(key);
}
}
// Usage
void main() async {
WidgetsFlutterBinding.ensureInitialized(); // Required for async operations before runApp
await SharedPreferencesHelper.saveString('username', 'FlutterDev');
final username = await SharedPreferencesHelper.getString('username');
print('Username: $username');
await SharedPreferencesHelper.remove('username');
}
2. Hive
Hive is a lightweight, NoSQL database that is written entirely in Dart. It’s an excellent choice for Flutter apps that need to store more structured data without the complexity of a full-fledged database system.
Pros
- Pure Dart: No native dependencies, simplifying cross-platform deployment.
- Performance: Fast read and write operations.
- Encryption Support: Built-in encryption for data security.
- Object Storage: Can store Dart objects directly.
- Easy to Use: Simple API for storing and retrieving data.
Cons
- NoSQL: Lacks relational data capabilities.
- Limited Querying: Simple key-based lookups, not suitable for complex queries.
- Size Limitations: Not ideal for extremely large datasets.
Usage
Step 1: Add Dependency
Add hive
and hive_flutter
dependencies to your pubspec.yaml
:
dependencies:
hive: ^2.2.3
hive_flutter: ^1.1.0
dev_dependencies:
build_runner: ^2.4.8
hive_generator: ^2.0.1
Step 2: Initialize Hive
import 'package:hive_flutter/hive_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
await Hive.openBox('myBox');
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Hive Example")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () async {
var box = Hive.box('myBox');
await box.put('name', 'FlutterDev');
print('Data saved');
},
child: Text('Save Data'),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
var box = Hive.box('myBox');
var name = box.get('name');
print('Name: $name');
},
child: Text('Load Data'),
),
],
),
),
);
}
}
Step 3 (Optional): Store Custom Objects (Type Adapters)
To store custom objects, you need to create a type adapter.
import 'package:hive/hive.dart';
part 'person.g.dart'; // This line is required. Must match file name.
@HiveType(typeId: 0) // Unique ID for this type.
class Person {
@HiveField(0) // Unique ID for this field.
String name;
@HiveField(1)
int age;
Person({required this.name, required this.age});
}
// Create a file called person.g.dart with the following content
// using command: flutter pub run build_runner build
Run flutter pub run build_runner build
to generate the adapter, then register it in main()
before opening boxes that store Person
objects.
import 'package:hive_flutter/hive_flutter.dart';
import 'person.dart'; // Import the generated file
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
Hive.registerAdapter(PersonAdapter()); //Register Adapter
await Hive.openBox('peopleBox');
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Hive Example")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () async {
final person = Person(name: "John Doe", age: 30);
var box = Hive.box('peopleBox');
await box.put('person1', person);
print('Data saved');
},
child: Text('Save Person'),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
var box = Hive.box('peopleBox');
var person = box.get('person1');
print('Person Name: ${person?.name}, Age: ${person?.age}');
},
child: Text('Load Person'),
),
],
),
),
);
}
}
3. Drift (formerly Moor)
Drift is a reactive persistence library for Flutter and Dart, providing a type-safe SQL experience. It wraps SQLite with a powerful ORM (Object-Relational Mapping) and supports code generation to provide compile-time checking and reduce boilerplate.
Pros
- Type Safety: Ensures data integrity through compile-time checks.
- Relational Data: Full support for SQL, allowing complex queries and relationships.
- Code Generation: Reduces boilerplate with automatic code generation.
- Reactive: Integration with Streams for real-time updates.
- Transactions: Built-in support for atomic transactions.
Cons
- Complexity: Steeper learning curve compared to SharedPreferences and Hive.
- SQLite Dependency: Relies on SQLite, which can be overkill for simple data storage needs.
- Setup Overhead: Requires more setup and configuration.
Usage
Step 1: Add Dependencies
Add Drift dependencies to your pubspec.yaml
:
dependencies:
drift: ^2.15.0
sqlite3_flutter_libs: ^0.5.0 # Optional, but recommended on Flutter
dev_dependencies:
drift_dev: ^2.15.0
build_runner: ^2.4.8
Step 2: Define Database Schema
Create a Dart file to define your database schema (e.g., database.dart
):
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart'
show getApplicationDocumentsDirectory;
import 'dart:io';
import 'package:path/path.dart' as p;
part 'database.g.dart';
class Tasks extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get description => text().named('desc')();
BoolColumn get completed => boolean().withDefault(const Constant(false))();
}
@DriftDatabase(tables: [Tasks])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
}
LazyDatabase _openConnection() {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'db.sqlite'));
return NativeDatabase(file);
});
}
Step 3: Generate Code
Run flutter pub run build_runner build
to generate the necessary Drift code.
Step 4: Use the Database
import 'package:flutter/material.dart';
import 'database.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State {
late AppDatabase database;
@override
void initState() {
super.initState();
database = AppDatabase();
}
@override
void dispose() {
database.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Drift Example'),
),
body: StreamBuilder>(
stream: database.select(database.tasks).watch(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final tasks = snapshot.data!;
return ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
final task = tasks[index];
return ListTile(
title: Text(task.description),
trailing: Checkbox(
value: task.completed,
onChanged: (newValue) {
database.update(database.tasks)
..where((tbl) => tbl.id.equals(task.id))
..write(TasksCompanion(completed: Value(newValue!)));
},
),
);
},
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
showDialog(
context: context,
builder: (context) {
final controller = TextEditingController();
return AlertDialog(
title: Text('Add Task'),
content: TextField(
controller: controller,
decoration: InputDecoration(hintText: 'Task description'),
),
actions: [
TextButton(
child: Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: Text('Add'),
onPressed: () {
database.into(database.tasks).insert(
TasksCompanion(
description: Value(controller.text),
),
);
Navigator.of(context).pop();
},
),
],
);
},
);
},
),
),
);
}
}
Summary Table: Hive vs. Drift vs. SharedPreferences
Feature | SharedPreferences | Hive | Drift |
---|---|---|---|
Data Type | Simple key-value pairs | Dart Objects, simple values | SQL data types |
Data Complexity | Basic | Moderate | Complex Relational Data |
Querying | None | Key-based | SQL |
Transactions | No | No | Yes |
Real-Time Updates | No | No | Yes (with Streams) |
Type Safety | No | No | Yes |
Learning Curve | Easy | Moderate | Steep |
Dependencies | shared_preferences |
hive , hive_flutter |
drift , sqlite3 , build_runner |
Use Cases | Simple app settings, user preferences | Structured data, caching | Complex data models, offline support |
Conclusion
Choosing the right local data storage solution in Flutter depends on the requirements of your application. SharedPreferences are best for very simple configurations and small amounts of data. Hive is ideal for applications needing fast, lightweight storage with Dart object support. Drift is suitable for complex applications with relational data models, requiring type safety and transactional support. Understanding the trade-offs of each option allows you to make an informed decision and build more efficient and robust Flutter applications.