Skip to content

Flutter Local Storage — SharedPreferences, SQLite, and File Storage

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Flutter Local Storage. We cover key concepts, practical examples, and best practices to help you master this topic.

Flutter local storage solutions enable persisting data on the device between app sessions, from simple key-value preferences with SharedPreferences to structured relational data with SQLite (sqflite) and arbitrary file storage.

What Will You Learn

  • SharedPreferences for simple key-value storage
  • sqflite for SQLite database operations
  • File storage for documents and images
  • Reading and writing text and binary files
  • Storing complex objects as JSON
  • Path providers for accessing directories

Why It Matters

Local storage is essential for offline functionality, user preferences, and caching. SharedPreferences is perfect for settings and small data. sqflite handles structured data like contacts or notes. File storage stores images, documents, and cached network responses. Choosing the right storage solution affects app performance, data integrity, and user experience.

Real-World Use

The DodaTech Flutter app uses SharedPreferences for user preferences (theme, language, notification settings), sqflite for the course progress database (completed lessons, quiz scores, bookmarks), and file storage for downloading course materials (PDFs, videos). The file cache uses the path_provider package to access app-specific directories.

Learning Path

flowchart LR
  A[Flutter Animations] --> B[Local Storage\nYou are here]
  B --> C[Firebase Integration]
  style B fill:#f90,color:#fff

SharedPreferences

SharedPreferences stores key-value pairs persistently:

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class SettingsService {
  Future<void> saveThemeMode(String mode) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('theme_mode', mode);
  }

  Future<String> getThemeMode() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString('theme_mode') ?? 'system';
  }

  Future<void> saveUserName(String name) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('user_name', name);
  }

  Future<String?> getUserName() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString('user_name');
  }

  Future<void> clearAll() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.clear();
  }
}

// Usage in a widget:
class SettingsScreen extends StatefulWidget {
  @override
  State<SettingsScreen> createState() => _SettingsScreenState();
}

class _SettingsScreenState extends State<SettingsScreen> {
  final _settingsService = SettingsService();
  String _userName = '';

  @override
  void initState() {
    super.initState();
    _loadSettings();
  }

  Future<void> _loadSettings() async {
    final name = await _settingsService.getUserName();
    setState(() => _userName = name ?? 'Guest');
  }

  Future<void> _saveName(String name) async {
    await _settingsService.saveUserName(name);
    setState(() => _userName = name);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Settings')),
      body: Padding(
        padding: EdgeInsets.all(24),
        child: Column(
          children: [
            Text('Welcome, $_userName'),
            SizedBox(height: 16),
            TextField(
              decoration: InputDecoration(
                labelText: 'Enter your name',
                border: OutlineInputBorder(),
              ),
              onSubmitted: (value) => _saveName(value.trim()),
            ),
          ],
        ),
      ),
    );
  }
}

SharedPreferences supports String, int, double, bool, and List<String>. All operations are asynchronous. Use it for small amounts of data (under 100KB).

Storing Complex Objects

Serialize objects to JSON for SharedPreferences:

import 'dart:convert';

class UserPreferences {
  final String name;
  final bool notificationsEnabled;
  final double fontSize;
  final List<String> favoriteCategories;

  UserPreferences({
    required this.name,
    required this.notificationsEnabled,
    required this.fontSize,
    required this.favoriteCategories,
  });

  Map<String, dynamic> toJson() => {
    'name': name,
    'notificationsEnabled': notificationsEnabled,
    'fontSize': fontSize,
    'favoriteCategories': favoriteCategories,
  };

  factory UserPreferences.fromJson(Map<String, dynamic> json) => UserPreferences(
    name: json['name'] as String,
    notificationsEnabled: json['notificationsEnabled'] as bool,
    fontSize: (json['fontSize'] as num).toDouble(),
    favoriteCategories: List<String>.from(json['favoriteCategories']),
  );

  Future<void> save() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('user_prefs', jsonEncode(toJson()));
  }

  static Future<UserPreferences?> load() async {
    final prefs = await SharedPreferences.getInstance();
    final json = prefs.getString('user_prefs');
    if (json == null) return null;
    return UserPreferences.fromJson(jsonDecode(json));
  }
}

Serialize complex preferences as JSON strings. This pattern stores entire objects while keeping the API clean.

SQLite with sqflite

sqflite provides relational database storage:

import 'package:flutter/material.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class DatabaseHelper {
  static final DatabaseHelper _instance = DatabaseHelper._internal();
  factory DatabaseHelper() => _instance;
  DatabaseHelper._internal();

  static Database? _database;

  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDatabase();
    return _database!;
  }

  Future<Database> _initDatabase() async {
    final dbPath = await getDatabasesPath();
    final path = join(dbPath, 'notes_app.db');

    return await openDatabase(
      path,
      version: 1,
      onCreate: _onCreate,
    );
  }

  Future<void> _onCreate(Database db, int version) async {
    await db.execute('''
      CREATE TABLE notes(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        title TEXT NOT NULL,
        content TEXT,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL,
        is_favorite INTEGER DEFAULT 0
      )
    ''');

    await db.execute('''
      CREATE TABLE tags(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL UNIQUE
      )
    ''');

    await db.execute('''
      CREATE TABLE note_tags(
        note_id INTEGER,
        tag_id INTEGER,
        PRIMARY KEY (note_id, tag_id),
        FOREIGN KEY (note_id) REFERENCES notes(id),
        FOREIGN KEY (tag_id) REFERENCES tags(id)
      )
    ''');
  }

  // CRUD operations
  Future<int> insertNote(Map<String, dynamic> note) async {
    final db = await database;
    return await db.insert('notes', note);
  }

  Future<List<Map<String, dynamic>>> getAllNotes() async {
    final db = await database;
    return await db.query('notes', orderBy: 'updated_at DESC');
  }

  Future<int> updateNote(int id, Map<String, dynamic> note) async {
    final db = await database;
    return await db.update('notes', note, where: 'id = ?', whereArgs: [id]);
  }

  Future<int> deleteNote(int id) async {
    final db = await database;
    return await db.delete('notes', where: 'id = ?', whereArgs: [id]);
  }

  // Search notes
  Future<List<Map<String, dynamic>>> searchNotes(String query) async {
    final db = await database;
    return await db.query(
      'notes',
      where: 'title LIKE ? OR content LIKE ?',
      whereArgs: ['%$query%', '%$query%'],
      orderBy: 'updated_at DESC',
    );
  }

  Future<List<Map<String, dynamic>>> getFavoriteNotes() async {
    final db = await database;
    return await db.query(
      'notes',
      where: 'is_favorite = ?',
      whereArgs: [1],
      orderBy: 'updated_at DESC',
    );
  }
}

// Usage:
// final db = DatabaseHelper();
// await db.insertNote({'title': 'My Note', 'content': 'Hello', 'created_at': DateTime.now().toIso8601String(), 'updated_at': DateTime.now().toIso8601String()});

Use the Singleton pattern for the database helper. The onCreate callback runs once when the database is first created. Use parameterized queries (?) to prevent SQL Injection.

Using the Database in a Widget

Integrate the database with a ListView:

class NotesScreen extends StatefulWidget {
  @override
  State<NotesScreen> createState() => _NotesScreenState();
}

class _NotesScreenState extends State<NotesScreen> {
  final _dbHelper = DatabaseHelper();
  List<Map<String, dynamic>> _notes = [];
  bool _isLoading = true;

  @override
  void initState() {
    super.initState();
    _loadNotes();
  }

  Future<void> _loadNotes() async {
    setState(() => _isLoading = true);
    final notes = await _dbHelper.getAllNotes();
    setState(() {
      _notes = notes;
      _isLoading = false;
    });
  }

  Future<void> _addNote() async {
    await _dbHelper.insertNote({
      'title': 'New Note ${DateTime.now().second}',
      'content': 'Content here',
      'created_at': DateTime.now().toIso8601String(),
      'updated_at': DateTime.now().toIso8601String(),
      'is_favorite': 0,
    });
    await _loadNotes();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Notes')),
      body: _isLoading
          ? Center(child: CircularProgressIndicator())
          : ListView.builder(
              itemCount: _notes.length,
              itemBuilder: (_, i) => ListTile(
                title: Text(_notes[i]['title']),
                subtitle: Text(_notes[i]['content']),
                trailing: Icon(
                  Icons.favorite,
                  color: _notes[i]['is_favorite'] == 1 ? Colors.red : Colors.grey,
                ),
              ),
            ),
      floatingActionButton: FloatingActionButton(
        onPressed: _addNote,
        child: Icon(Icons.add),
      ),
    );
  }
}

Always reload data from the database after mutations. Use FutureBuilder or explicit setState calls to refresh the UI.

File Storage

Store files in the app's documents or temporary directory:

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';

class FileStorageService {
  Future<Directory> get _documentsDir async =>
      await getApplicationDocumentsDirectory();

  Future<Directory> get _tempDir async =>
      await getTemporaryDirectory();

  Future<File> get _dataFile async {
    final dir = await _documentsDir;
    return File('${dir.path}/app_data.json');
  }

  Future<String> readString(String fileName) async {
    final dir = await _documentsDir;
    final file = File('${dir.path}/$fileName');
    if (!await file.exists()) return '';
    return await file.readAsString();
  }

  Future<void> writeString(String fileName, String content) async {
    final dir = await _documentsDir;
    final file = File('${dir.path}/$fileName');
    await file.writeAsString(content);
  }

  Future<File> saveImage(String fileName, List<int> bytes) async {
    final dir = await _documentsDir;
    final file = File('${dir.path}/$fileName');
    return await file.writeAsBytes(bytes);
  }

  Future<List<int>> readImage(String fileName) async {
    final dir = await _documentsDir;
    final file = File('${dir.path}/$fileName');
    if (!await file.exists()) throw FileSystemException('File not found');
    return await file.readAsBytes();
  }

  Future<bool> deleteFile(String fileName) async {
    final dir = await _documentsDir;
    final file = File('${dir.path}/$fileName');
    if (await file.exists()) {
      await file.delete();
      return true;
    }
    return false;
  }

  Future<void> clearTempDirectory() async {
    final dir = await _tempDir;
    if (await dir.exists()) {
      await dir.delete(recursive: true);
      await dir.create();
    }
  }

  Future<int> getStorageUsed() async {
    final dir = await _documentsDir;
    int totalSize = 0;
    await for (final entity in dir.list(recursive: true)) {
      if (entity is File) {
        totalSize += await entity.length();
      }
    }
    return totalSize;
  }
}

// Usage:
// final fileService = FileStorageService();
// await fileService.writeString('profile.json', jsonData);
// final data = await fileService.readString('profile.json');

Use path_provider to get platform-specific directories. getApplicationDocumentsDirectory is for persistent data. getTemporaryDirectory is for cache files that the system may delete.

Hive Lightweight Database

Hive is a lightweight, fast key-value database:

import 'package:hive_flutter/hive_flutter.dart';

class HiveStorageService {
  static Future<void> init() async {
    await Hive.initFlutter();
  }

  Future<void> saveData(String boxName, String key, dynamic value) async {
    final box = await Hive.openBox(boxName);
    await box.put(key, value);
  }

  Future<dynamic> getData(String boxName, String key) async {
    final box = await Hive.openBox(boxName);
    return box.get(key);
  }

  Future<void> deleteData(String boxName, String key) async {
    final box = await Hive.openBox(boxName);
    await box.delete(key);
  }

  Future<void> clearBox(String boxName) async {
    final box = await Hive.openBox(boxName);
    await box.clear();
  }

  // Type-safe storage with Hive objects
  Future<void> saveUser(User user) async {
    final box = await Hive.openBox<User>('users');
    await box.put(user.id, user);
  }

  Future<User?> getUser(String id) async {
    final box = await Hive.openBox<User>('users');
    return box.get(id);
  }
}

// Hive TypeAdapter generated with build_runner:
// @HiveType(typeId: 0)
// class User extends HiveObject {
//   @HiveField(0)
//   String id;
//   @HiveField(1)
//   String name;
//   @HiveField(2)
//   int age;
// }

Hive is faster than SharedPreferences for complex data. It supports lazy boxes, encrypted boxes, and custom type adapters for object storage.

Common Mistakes

  1. Not initializing storage before use: SharedPreferences and sqflite require async initialization. Ensure they are initialized before accessing data, especially in main().

  2. Blocking the UI thread with synchronous file I/O: All file and database operations are async. Use await and show loading states.

  3. Not closing databases: Close the database when the app terminates. However, sqflite manages this automatically for most use cases.

  4. Storing large objects in SharedPreferences: SharedPreferences is not designed for large data. Use sqflite or file storage for data over 100KB.

  5. Hardcoding file paths: Use path_provider to get platform-appropriate directories. Hardcoding paths breaks on different platforms.

Practice Questions

  1. What kind of data is appropriate for SharedPreferences vs SQLite?
  2. How does the singleton pattern benefit database helper classes?
  3. Why should you use parameterized queries in SQLite?
  4. How does path_provider help with cross-platform file storage?
  5. Challenge: Build a journal app with sqflite. Store entries with title, content, mood (enum), date, and tags. Implement full CRUD, search by tag, and export as JSON to a file.

Mini Project

Build a todo list app with offline persistence:

  • Use SQLite (sqflite) for todo items
  • SharedPreferences for app preferences (sort order, dark mode)
  • File storage for exporting todos as CSV
  • Each todo has: title, description, due date, priority (high/medium/low), completed flag
  • CRUD operations with optimistic UI updates
  • Search and filter by priority
  • Export to CSV file in downloads directory

FAQ

What is the difference between sqflite and Hive?

sqflite provides full SQL query capabilities. Hive is a NoSQL key-value store that is faster for simple operations. Choose sqflite for complex queries, Hive for simple object persistence.

Can I store images in SQLite?

Technically yes (BLOB), but it is not recommended. Store images as files and save the file path in the database.

How do I migrate a SQLite database schema?

Increment the version parameter in openDatabase and provide an onUpgrade callback that executes ALTER TABLE statements.

Where are files stored on each platform?

Documents: Android data/data/.../files, iOS NSDocumentDirectory. Temporary: platform-appropriate temp directories. Use path_provider to abstract these.

How do I encrypt local storage?

Use flutter_secure_storage for sensitive key-value data, sqflite with encryption extensions, or Hive with Hive.openBox('box', encryptionKey: key).

What is Next

Now that you understand local storage, learn about Firebase integration. Proceed to Flutter Firebase Integration for authentication, Firestore, and cloud functions. Then explore Flutter Testing for unit, widget, and integration tests.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro