← Back to Articles

Flutter Local Storage: Persisting Data with SharedPreferences, Hive, and SQLite

Flutter Local Storage: Persisting Data with SharedPreferences, Hive, and SQLite

Flutter Local Storage: Persisting Data with SharedPreferences, Hive, and SQLite

When building Flutter apps, you'll often need to save data locally on the device. Whether it's user preferences, cached content, or complex relational data, choosing the right storage solution is crucial. In this article, we'll explore three popular options: SharedPreferences for simple key-value pairs, Hive for fast NoSQL storage, and SQLite for structured relational data.

Why Local Storage Matters

Local storage allows your app to work offline, improve performance by caching data, and provide a better user experience by remembering preferences. Each storage solution has its strengths, and understanding when to use which one will help you build more efficient apps.

Let's start with the simplest option and work our way up to more complex solutions.

Storage Solutions Overview

Here's a visual representation of the three storage solutions and their complexity levels:

SharedPreferences Simple Key-Value Hive NoSQL Boxes SQLite Relational DB

SharedPreferences: Simple Key-Value Storage

SharedPreferences is perfect for storing small pieces of data like user settings, theme preferences, or simple flags. It's lightweight, easy to use, and built into Flutter through the shared_preferences package.

Setting Up SharedPreferences

First, add the package to your pubspec.yaml:


dependencies:
  shared_preferences: ^2.2.2

Then import it in your Dart file:


import 'package:shared_preferences/shared_preferences.dart';

Reading and Writing Data

SharedPreferences works asynchronously. Here's how to save and retrieve data:

Data flow in SharedPreferences:

Flutter App setString() SharedPreferences Key-Value Store Device Storage

class SettingsService {
  static const String _themeKey = 'theme';
  static const String _usernameKey = 'username';
  
  Future saveTheme(String theme) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_themeKey, theme);
  }
  
  Future getTheme() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(_themeKey);
  }
  
  Future saveUsername(String username) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_usernameKey, username);
  }
  
  Future getUsername() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(_usernameKey);
  }
  
  Future clearAll() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.clear();
  }
}

SharedPreferences supports various data types: String, int, double, bool, and List<String>. Here's an example using different types:


Future saveUserPreferences() async {
  final prefs = await SharedPreferences.getInstance();
  
  await prefs.setString('name', 'John Doe');
  await prefs.setInt('age', 30);
  await prefs.setDouble('score', 95.5);
  await prefs.setBool('isPremium', true);
  await prefs.setStringList('favorites', ['flutter', 'dart', 'mobile']);
}

Future> getUserPreferences() async {
  final prefs = await SharedPreferences.getInstance();
  
  return {
    'name': prefs.getString('name'),
    'age': prefs.getInt('age'),
    'score': prefs.getDouble('score'),
    'isPremium': prefs.getBool('isPremium'),
    'favorites': prefs.getStringList('favorites'),
  };
}

When to Use SharedPreferences

Use SharedPreferences when you need to store:

  • User preferences and settings
  • Simple flags or boolean values
  • Small amounts of string data
  • Data that doesn't require complex queries

Avoid SharedPreferences for large amounts of data or when you need to perform complex queries or relationships between data.

Hive: Fast NoSQL Database

Hive is a lightweight, fast NoSQL database written in pure Dart. It's perfect when you need more than SharedPreferences but don't need the complexity of SQLite. Hive stores data in boxes (similar to tables) and is incredibly fast because it's written entirely in Dart.

Setting Up Hive

Add Hive to your pubspec.yaml:


dependencies:
  hive: ^2.2.3
  hive_flutter: ^1.1.0

dev_dependencies:
  hive_generator: ^2.0.1
  build_runner: ^2.4.7

Initialize Hive in your main function:


import 'package:hive_flutter/hive_flutter.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Hive.initFlutter();
  runApp(MyApp());
}

Creating a Hive Model

First, create a model class. For simple cases, you can use HiveObject:


import 'package:hive/hive.dart';

@HiveType(typeId: 0)
class User extends HiveObject {
  @HiveField(0)
  String name;
  
  @HiveField(1)
  int age;
  
  @HiveField(2)
  String email;
  
  User({
    required this.name,
    required this.age,
    required this.email,
  });
}

Generate the adapter by running:


flutter pub run build_runner build

Using Hive Boxes

Open a box and register your adapter:

Hive organizes data into boxes, similar to tables:

Hive Database Storage Engine Users Box User Objects Notes Box Note Objects Cache Box Cached Data

class UserService {
  static const String _boxName = 'users';
  Box? _userBox;
  
  Future init() async {
    Hive.registerAdapter(UserAdapter());
    _userBox = await Hive.openBox(_boxName);
  }
  
  Future addUser(User user) async {
    await _userBox?.add(user);
  }
  
  Future> getAllUsers() async {
    return _userBox?.values.toList() ?? [];
  }
  
  Future getUser(int index) async {
    return _userBox?.getAt(index);
  }
  
  Future updateUser(int index, User user) async {
    await _userBox?.putAt(index, user);
  }
  
  Future deleteUser(int index) async {
    await _userBox?.deleteAt(index);
  }
  
  Future clearAll() async {
    await _userBox?.clear();
  }
}

Using Hive with Type Adapters

For simple data types, you can use Hive without code generation:


class NoteService {
  static const String _boxName = 'notes';
  Box? _noteBox;
  
  Future init() async {
    _noteBox = await Hive.openBox(_boxName);
  }
  
  Future saveNote(String key, Map note) async {
    await _noteBox?.put(key, note);
  }
  
  Future?> getNote(String key) async {
    return _noteBox?.get(key);
  }
  
  Future>> getAllNotes() async {
    return _noteBox?.values.cast>().toList();
  }
  
  Future deleteNote(String key) async {
    await _noteBox?.delete(key);
  }
}

When to Use Hive

Hive is ideal when you need:

  • Fast read and write operations
  • Structured data that doesn't fit SharedPreferences
  • Type-safe storage with code generation
  • A lightweight alternative to SQLite
  • Cross-platform compatibility (works on all Flutter platforms)

SQLite: Relational Database

SQLite is a powerful relational database perfect for complex data with relationships, queries, and transactions. In Flutter, we use the sqflite package to work with SQLite databases.

Setting Up SQLite

Add sqflite and path packages:


dependencies:
  sqflite: ^2.3.0
  path: ^1.8.3

Creating a Database Helper

Let's create a database helper class to manage our SQLite database:


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 NOT NULL UNIQUE,
        age INTEGER,
        created_at TEXT NOT NULL
      )
    ''');
    
    await db.execute('''
      CREATE TABLE posts (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id INTEGER NOT NULL,
        title TEXT NOT NULL,
        content TEXT,
        created_at TEXT NOT NULL,
        FOREIGN KEY (user_id) REFERENCES users (id)
      )
    ''');
  }
}

Creating Model Classes

Create model classes to represent your data:


class User {
  final int? id;
  final String name;
  final String email;
  final int? age;
  final String createdAt;
  
  User({
    this.id,
    required this.name,
    required this.email,
    this.age,
    required this.createdAt,
  });
  
  Map toMap() {
    return {
      'id': id,
      'name': name,
      'email': email,
      'age': age,
      'created_at': createdAt,
    };
  }
  
  factory User.fromMap(Map map) {
    return User(
      id: map['id'] as int?,
      name: map['name'] as String,
      email: map['email'] as String,
      age: map['age'] as int?,
      createdAt: map['created_at'] as String,
    );
  }
}

CRUD Operations with SQLite

Implement create, read, update, and delete operations:

SQLite uses relational tables with foreign key relationships:

SQLite Database Relational Storage Users Table id, name, email Posts Table id, user_id, title FOREIGN KEY JOIN Queries Relational Data

class UserDao {
  final DatabaseHelper dbHelper = DatabaseHelper.instance;
  
  Future insertUser(User user) async {
    final db = await dbHelper.database;
    return await db.insert('users', user.toMap());
  }
  
  Future> getAllUsers() async {
    final db = await dbHelper.database;
    final List> maps = await db.query('users');
    return List.generate(maps.length, (i) => User.fromMap(maps[i]));
  }
  
  Future getUserById(int id) async {
    final db = await dbHelper.database;
    final List> maps = await db.query(
      'users',
      where: 'id = ?',
      whereArgs: [id],
    );
    
    if (maps.isNotEmpty) {
      return User.fromMap(maps.first);
    }
    return null;
  }
  
  Future> searchUsers(String query) async {
    final db = await dbHelper.database;
    final List> maps = await db.query(
      'users',
      where: 'name LIKE ? OR email LIKE ?',
      whereArgs: ['%$query%', '%$query%'],
    );
    return List.generate(maps.length, (i) => User.fromMap(maps[i]));
  }
  
  Future updateUser(User user) async {
    final db = await dbHelper.database;
    return await db.update(
      'users',
      user.toMap(),
      where: 'id = ?',
      whereArgs: [user.id],
    );
  }
  
  Future deleteUser(int id) async {
    final db = await dbHelper.database;
    return await db.delete(
      'users',
      where: 'id = ?',
      whereArgs: [id],
    );
  }
  
  Future>> getUsersWithPosts() async {
    final db = await dbHelper.database;
    return await db.rawQuery('''
      SELECT users.*, COUNT(posts.id) as post_count
      FROM users
      LEFT JOIN posts ON users.id = posts.user_id
      GROUP BY users.id
    ''');
  }
}

Using Transactions

SQLite supports transactions for atomic operations:


Future transferData() async {
  final db = await DatabaseHelper.instance.database;
  
  await db.transaction((txn) async {
    // Multiple operations that must succeed or fail together
    await txn.insert('users', user1.toMap());
    await txn.insert('users', user2.toMap());
    await txn.insert('posts', post1.toMap());
  });
}

When to Use SQLite

SQLite is the right choice when you need:

  • Complex relational data with foreign keys
  • Advanced queries with JOINs, aggregations, and filtering
  • Transactions for data integrity
  • Large datasets with efficient indexing
  • Data that requires complex relationships between entities

Comparing the Three Options

Here's a quick comparison to help you choose:

Visual comparison of complexity and use cases:

SharedPreferences Simple Settings Preferences Hive Moderate Objects Caching SQLite Complex Relations Queries Simple Complex

SharedPreferences: Best for simple key-value pairs, user preferences, and small amounts of data. Very easy to use but limited in functionality.

Hive: Excellent for structured data that needs fast access. Great balance between simplicity and functionality. Perfect for caching and storing objects.

SQLite: The most powerful option for complex data with relationships. Ideal for apps that need sophisticated queries and data integrity through transactions.

Best Practices

Regardless of which storage solution you choose, follow these best practices:

  • Initialize early: Initialize your storage solution in your app's initialization code, not when first needed.
  • Handle errors: Always wrap storage operations in try-catch blocks to handle potential errors gracefully.
  • Don't store sensitive data: Never store passwords, API keys, or other sensitive information in local storage without encryption.
  • Clean up: Periodically clean up old or unused data to prevent your app from consuming too much storage.
  • Use migrations: When using SQLite or Hive, plan for database schema changes and implement migrations.
  • Test thoroughly: Test your storage operations, especially error cases and edge conditions.

Conclusion

Choosing the right local storage solution depends on your app's specific needs. Start simple with SharedPreferences for basic preferences, upgrade to Hive for structured data that needs fast access, and use SQLite when you need the full power of a relational database. Understanding these three options gives you the flexibility to make the right choice for each part of your Flutter app.

Remember, you can even use multiple storage solutions in the same app. For example, use SharedPreferences for user settings, Hive for caching API responses, and SQLite for complex relational data. The key is choosing the right tool for each job.

Happy coding, and may your data persist reliably!