Flutter Local Storage and Data Persistence: Storing Data on Device
When building Flutter apps, you'll often need to store data locally on the user's device. Whether it's user preferences, cached data, or complex relational information, Flutter offers several options for local storage. In this article, we'll explore the most common approaches and help you choose the right one for your needs.
Why Local Storage Matters
Local storage allows your app to:
- Remember user preferences and settings
- Cache data for offline access
- Store user-generated content
- Improve app performance by reducing network calls
- Provide a better user experience with instant data access
Each storage solution has its strengths, and understanding when to use each one will help you build more efficient and user-friendly apps.
SharedPreferences: Simple Key-Value Storage
SharedPreferences is perfect for storing simple data types like strings, integers, booleans, and doubles. It's ideal for user preferences, settings, and small amounts of data.
To use SharedPreferences, add the package to your pubspec.yaml:
dependencies:
shared_preferences: ^2.2.2
Here's a basic example of saving and retrieving data:
import 'package:shared_preferences/shared_preferences.dart';
class PreferencesService {
static Future saveUsername(String username) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('username', username);
}
static Future getUsername() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('username');
}
static Future saveLoginStatus(bool isLoggedIn) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('is_logged_in', isLoggedIn);
}
static Future getLoginStatus() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool('is_logged_in') ?? false;
}
}
SharedPreferences works asynchronously, so you'll use await when reading or writing data. It's simple and straightforward, but it's limited to primitive data types and isn't suitable for complex objects or large amounts of data.
Hive: Fast NoSQL Database
Hive is a lightweight, fast, and powerful NoSQL database written in pure Dart. It's perfect for storing complex objects, lists, and maps without the overhead of SQL. Hive is particularly great for apps that need to store structured data but don't require complex queries.
Add Hive to your project:
dependencies:
hive: ^2.2.3
hive_flutter: ^1.1.0
dev_dependencies:
hive_generator: ^2.0.1
build_runner: ^2.4.7
First, initialize Hive in your app:
import 'package:hive_flutter/hive_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
runApp(MyApp());
}
Create a model class and generate the Hive adapter:
import 'package:hive/hive.dart';
part 'user_model.g.dart';
@HiveType(typeId: 0)
class UserModel extends HiveObject {
@HiveField(0)
String name;
@HiveField(1)
int age;
@HiveField(2)
List hobbies;
UserModel({
required this.name,
required this.age,
required this.hobbies,
});
}
After running flutter pub run build_runner build, you can use the model:
class UserService {
static const String boxName = 'usersBox';
static Box? _box;
static Future init() async {
Hive.registerAdapter(UserModelAdapter());
_box = await Hive.openBox(boxName);
}
static Future saveUser(UserModel user) async {
await _box?.put(user.name, user);
}
static UserModel? getUser(String name) {
return _box?.get(name);
}
static Future deleteUser(String name) async {
await _box?.delete(name);
}
static List getAllUsers() {
return _box?.values.toList() ?? [];
}
}
Hive is excellent for storing complex objects and provides better performance than SharedPreferences for larger datasets. It also supports encryption and works offline by default.
SQLite: Relational Database
For apps that need complex queries, relationships between data, and SQL capabilities, SQLite is the way to go. The sqflite package provides a SQLite database for Flutter.
Add the package:
dependencies:
sqflite: ^2.3.0
path: ^1.8.3
Here's how to set up and use SQLite:
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
static final DatabaseHelper instance = DatabaseHelper._init();
static Database? _database;
DatabaseHelper._init();
Future get database async {
if (_database != null) return _database!;
_database = await _initDB('app_database.db');
return _database!;
}
Future _initDB(String filePath) async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, filePath);
return await openDatabase(
path,
version: 1,
onCreate: _createDB,
);
}
Future _createDB(Database db, int version) async {
await db.execute('''
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at TEXT NOT NULL
)
''');
}
Future insertUser(Map user) async {
final db = await database;
return await db.insert('users', user);
}
Future>> getAllUsers() async {
final db = await database;
return await db.query('users');
}
Future>> getUserByEmail(String email) async {
final db = await database;
return await db.query(
'users',
where: 'email = ?',
whereArgs: [email],
);
}
Future updateUser(Map user) async {
final db = await database;
return await db.update(
'users',
user,
where: 'id = ?',
whereArgs: [user['id']],
);
}
Future deleteUser(int id) async {
final db = await database;
return await db.delete(
'users',
where: 'id = ?',
whereArgs: [id],
);
}
}
SQLite is powerful but comes with more complexity. Use it when you need relational data, complex queries, or transactions.
File Storage: For Documents and Media
Sometimes you need to store files like images, PDFs, or custom data formats. The path_provider package helps you get the correct directories for storing files.
dependencies:
path_provider: ^2.1.1
Here's an example of saving and reading text files:
import 'dart:io';
import 'package:path_provider/path_provider.dart';
class FileStorageService {
static Future get _localPath async {
final directory = await getApplicationDocumentsDirectory();
return directory.path;
}
static Future get _localFile async {
final path = await _localPath;
return File('$path/data.txt');
}
static Future readData() async {
try {
final file = await _localFile;
final contents = await file.readAsString();
return contents;
} catch (e) {
return '';
}
}
static Future writeData(String data) async {
final file = await _localFile;
return file.writeAsString(data);
}
}
File storage is ideal for documents, images, cached media files, or any data that doesn't fit into a structured database format.
Choosing the Right Storage Solution
Here's a quick guide to help you choose:
- SharedPreferences: Use for simple key-value pairs like user settings, theme preferences, or login tokens. Best for small amounts of primitive data.
- Hive: Use for structured objects, lists, and maps that don't need complex queries. Great for caching API responses or storing user-generated content.
- SQLite: Use when you need relationships between data, complex queries, or transactions. Perfect for apps with complex data models like note-taking apps or task managers.
- File Storage: Use for documents, images, or custom file formats. Ideal for media files or exporting data.
Best Practices
Regardless of which storage solution you choose, follow these best practices:
- Handle errors gracefully: Storage operations can fail, so always wrap them in try-catch blocks.
- Don't block the UI: All storage operations are asynchronous, so use
awaitproperly and consider showing loading indicators. - Clean up old data: Implement logic to remove outdated cached data to prevent your app from using too much storage space.
- Encrypt sensitive data: For sensitive information like passwords or tokens, consider using encryption. Hive supports encryption out of the box.
- Test your storage logic: Write unit tests to ensure your data persistence works correctly, especially after app updates.
Putting It All Together
Here's a complete example that combines multiple storage approaches:
class StorageManager {
// SharedPreferences for simple settings
static Future saveTheme(String theme) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('theme', theme);
}
// Hive for user data
static Future saveUserProfile(UserModel user) async {
final box = await Hive.openBox('profiles');
await box.put('current_user', user);
}
// SQLite for complex relational data
static Future saveNotes(List notes) async {
final db = await DatabaseHelper.instance.database;
final batch = db.batch();
for (var note in notes) {
batch.insert('notes', note.toMap());
}
await batch.commit();
}
// File storage for documents
static Future exportData(String jsonData) async {
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/export.json');
await file.writeAsString(jsonData);
}
}
Many apps use a combination of storage solutions. For example, you might use SharedPreferences for settings, Hive for cached API data, SQLite for user-generated content, and file storage for exported documents.
Conclusion
Understanding Flutter's local storage options empowers you to build apps that work offline, remember user preferences, and provide a smooth user experience. Start with SharedPreferences for simple needs, consider Hive for structured objects, use SQLite for complex relational data, and leverage file storage for documents and media.
Each solution has its place, and the best Flutter apps often combine multiple approaches. As you build more apps, you'll develop a sense of which storage solution fits each use case. Happy coding!