Implementing Local Data Persistence in Flutter

In Flutter, local data persistence is a fundamental requirement for many applications. Whether it’s caching user preferences, storing offline data, or maintaining state across sessions, having reliable local storage is essential. This blog post delves into various methods of implementing local data persistence in Flutter, complete with detailed examples and best practices.

Why Local Data Persistence is Important

  • Offline Availability: Allows users to access data even without an internet connection.
  • Performance: Caching data locally improves app performance by reducing the need for frequent network requests.
  • User Preferences: Stores user settings and preferences, providing a personalized experience.
  • State Management: Persists application state, ensuring continuity between sessions.

Methods for Implementing Local Data Persistence in Flutter

1. Shared Preferences

Shared Preferences is the simplest way to store key-value pairs of primitive data types. It’s suitable for basic configurations and user preferences.

Adding the Dependency

First, add the shared_preferences package to your pubspec.yaml file:

dependencies:
  shared_preferences: ^2.2.2
Storing Data

Here’s how you can store data using Shared Preferences:

import 'package:shared_preferences/shared_preferences.dart';

class SharedPreferencesHelper {
  static Future saveData(String key, String value) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(key, value);
  }
}

// Example Usage:
Future saveUserPreference() async {
  await SharedPreferencesHelper.saveData('theme', 'dark');
}
Retrieving Data

To retrieve the stored data:

import 'package:shared_preferences/shared_preferences.dart';

class SharedPreferencesHelper {
  static Future getData(String key) async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(key);
  }
}

// Example Usage:
Future loadUserPreference() async {
  final theme = await SharedPreferencesHelper.getData('theme');
  print('User theme: $theme');
}
Limitations
  • Only supports primitive data types (int, double, bool, String, and StringList).
  • Not suitable for complex data structures or large datasets.
  • Synchronous API with asynchronous underpinnings; not ideal for UI responsiveness with large data.

2. SQLite

SQLite is a lightweight, disk-based database engine that requires no separate server process. It’s ideal for storing structured data locally in Flutter applications.

Adding the Dependency

Add the sqflite package to your pubspec.yaml file:

dependencies:
  sqflite: ^2.4.0
  path_provider: ^2.1.0
Setting Up the Database

Create a database helper class to manage the database operations:

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';

class DatabaseHelper {
  static Database? _database;

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

  Future _initDatabase() async {
    final documentsDirectory = await getApplicationDocumentsDirectory();
    final path = join(documentsDirectory.path, 'my_database.db');
    return openDatabase(
      path,
      version: 1,
      onCreate: _onCreate,
    );
  }

  Future _onCreate(Database db, int version) async {
    await db.execute('''
      CREATE TABLE items (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT,
        description TEXT
      )
    ''');
  }

  // Insert, query, update, delete methods will be added below
}
Performing CRUD Operations
Inserting Data
class DatabaseHelper {
  // ... (previous code)

  Future insertItem(Item item) async {
    final db = await database;
    return await db.insert(
      'items',
      item.toMap(),
      conflictAlgorithm: ConflictAlgorithm.replace,
    );
  }
}

class Item {
  final int? id;
  final String name;
  final String description;

  Item({this.id, required this.name, required this.description});

  Map toMap() {
    return {
      'id': id,
      'name': name,
      'description': description,
    };
  }
}

// Example Usage:
Future addItemToDatabase() async {
  final dbHelper = DatabaseHelper();
  final newItem = Item(name: 'Example Item', description: 'This is an example item.');
  await dbHelper.insertItem(newItem);
  print('Item inserted successfully.');
}
Querying Data
class DatabaseHelper {
  // ... (previous code)

  Future<List> getItems() async {
    final db = await database;
    final List<Map> maps = await db.query('items');
    return List.generate(maps.length, (i) {
      return Item(
        id: maps[i]['id'],
        name: maps[i]['name'],
        description: maps[i]['description'],
      );
    });
  }
}

// Example Usage:
Future fetchItemsFromDatabase() async {
  final dbHelper = DatabaseHelper();
  final items = await dbHelper.getItems();
  for (var item in items) {
    print('Item ID: ${item.id}, Name: ${item.name}, Description: ${item.description}');
  }
}
Updating Data
class DatabaseHelper {
  // ... (previous code)

  Future updateItem(Item item) async {
    final db = await database;
    return await db.update(
      'items',
      item.toMap(),
      where: 'id = ?',
      whereArgs: [item.id],
    );
  }
}

// Example Usage:
Future updateItemInDatabase() async {
  final dbHelper = DatabaseHelper();
  final updatedItem = Item(id: 1, name: 'Updated Item', description: 'This item has been updated.');
  await dbHelper.updateItem(updatedItem);
  print('Item updated successfully.');
}
Deleting Data
class DatabaseHelper {
  // ... (previous code)

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

// Example Usage:
Future deleteItemFromDatabase() async {
  final dbHelper = DatabaseHelper();
  await dbHelper.deleteItem(1);
  print('Item deleted successfully.');
}
Advantages
  • Suitable for structured data and complex relationships.
  • Supports CRUD operations with SQL queries.
  • More efficient for large datasets compared to Shared Preferences.
Disadvantages
  • Requires setting up a database schema and managing SQL queries.
  • More complex implementation compared to Shared Preferences.

3. Hive

Hive is a lightweight and fast NoSQL database solution for Flutter. It is easy to use and doesn’t require complex setup like SQLite. Hive is perfect for caching and storing complex data structures.

Adding the Dependency

Add the hive and hive_flutter packages to your pubspec.yaml file:

dependencies:
  hive: ^2.2.3
  hive_flutter: ^1.1.0

dev_dependencies:
  build_runner: ^2.4.8
  hive_generator: ^2.0.1
Setting Up Hive
Initializing Hive

Initialize Hive in your main.dart file:

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

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Hive.initFlutter();
  // Register adapters here
  runApp(MyApp());
}
Creating a Hive Object

To store complex data, you need to create a Hive object and an adapter for it.

import 'package:hive/hive.dart';

part 'item.g.dart'; // This is important for code generation

@HiveType(typeId: 0)
class Item {
  @HiveField(0)
  final int id;

  @HiveField(1)
  final String name;

  @HiveField(2)
  final String description;

  Item({required this.id, required this.name, required this.description});
}

Create an adapter by running the following command in the terminal:

flutter pub run build_runner build
Registering the Adapter
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'item.dart'; // Import the Item class

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Hive.initFlutter();
  Hive.registerAdapter(ItemAdapter()); // Register the adapter
  runApp(MyApp());
}
Storing and Retrieving Data
Opening a Box
late Box itemsBox;

Future openItemsBox() async {
  itemsBox = await Hive.openBox('itemsBox');
}

// Example Usage:
Future initializeHive() async {
  await openItemsBox();
}
Storing Data
Future addItemToHive(Item item) async {
  await itemsBox.put(item.id, item);
  print('Item added to Hive.');
}

// Example Usage:
Future addNewItem() async {
  final newItem = Item(id: 1, name: 'Hive Item', description: 'This item is stored in Hive.');
  await addItemToHive(newItem);
}
Retrieving Data
Future getItemFromHive(int id) async {
  final item = itemsBox.get(id);
  print('Item from Hive: ${item?.name}');
  return item;
}

// Example Usage:
Future fetchItem() async {
  final item = await getItemFromHive(1);
  if (item != null) {
    print('Item Name: ${item.name}, Description: ${item.description}');
  } else {
    print('Item not found.');
  }
}
Updating Data
Future updateItemInHive(Item updatedItem) async {
  await itemsBox.put(updatedItem.id, updatedItem);
  print('Item updated in Hive.');
}

// Example Usage:
Future modifyItem() async {
  final updatedItem = Item(id: 1, name: 'Updated Hive Item', description: 'This item has been updated in Hive.');
  await updateItemInHive(updatedItem);
}
Deleting Data
Future deleteItemFromHive(int id) async {
  await itemsBox.delete(id);
  print('Item deleted from Hive.');
}

// Example Usage:
Future removeItem() async {
  await deleteItemFromHive(1);
}
Advantages
  • Simple and easy to use API.
  • Fast read and write operations.
  • Supports complex data types and nested objects.
  • No SQL knowledge required.
Disadvantages
  • Relatively new compared to SQLite, so the community and resources are smaller.

4. Isar

Isar is another NoSQL database for Flutter. It’s known for being fast and easy to use, making it an excellent option for apps that require speed and simplicity when dealing with local data.

Adding the Dependency

First, you’ll need to add Isar and its necessary dependencies to your pubspec.yaml file:

dependencies:
  isar: ^4.0.0 # Or the latest version
  isar_flutter_libs: ^4.0.0

dev_dependencies:
  isar_generator: ^4.0.0
  build_runner: ^2.4.8

Then, run flutter pub get to install the dependencies.

Setting Up Isar
Initialize Isar

To get started, you’ll initialize Isar in your app.

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

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final dir = await getApplicationDocumentsDirectory();
  final isar = await Isar.open(
    [YourObjectClassSchema], // Replace with your actual schema class
    directory: dir.path,
  );

  runApp(MyApp(isar: isar));
}

class MyApp extends StatelessWidget {
  final Isar isar;

  MyApp({required this.isar});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // ...
    );
  }
}
Define Data Models

Here’s an example data model with annotations:

import 'package:isar/isar.dart';

part 'your_object_class.g.dart';

@collection
class YourObjectClass {
  Id? id = Isar.autoIncrement; // you can also use int

  String? name;

  int? age;
}

Then run flutter pub run build_runner build to generate the Isar schema.

Storing and Retrieving Data
Writing Data
final yourObject = YourObjectClass()
  ..name = 'John Doe'
  ..age = 30;

await isar.writeTxn(() async {
  await isar.yourObjectClasss.put(yourObject); // insert & update
});
Reading Data
final yourObject = await isar.yourObjectClasss.get(yourObjectId);

print('Name: ${yourObject?.name}, Age: ${yourObject?.age}');
Querying Data
final results = await isar.yourObjectClasss
  .where()
  .ageGreaterThan(18)
  .findAll();

for (var item in results) {
  print('Name: ${item.name}, Age: ${item.age}');
}
Updating Data
await isar.writeTxn(() async {
  final yourObject = await isar.yourObjectClasss.get(yourObjectId);
  if (yourObject != null) {
    yourObject.age = 31;
    await isar.yourObjectClasss.put(yourObject);
  }
});
Deleting Data
await isar.writeTxn(() async {
  await isar.yourObjectClasss.delete(yourObjectId);
});
Advantages
  • Extremely fast due to its efficient binary format.
  • Simple query system.
  • Fully cross-platform: works on Android, iOS, and Desktop platforms.
Disadvantages
  • Requires understanding of asynchronous programming for data operations.
  • Code generation steps must be correctly implemented.

Choosing the Right Approach

The choice of local data persistence method depends on your application’s requirements:

  • Use Shared Preferences for simple key-value storage of primitive data types.
  • Opt for SQLite when you need a structured database to store and manage complex data.
  • Choose Hive for fast, simple NoSQL storage that supports complex data types.
  • Choose Isar for when speed and ease of use with complex querying and data management are important

Best Practices for Local Data Persistence

  • Security: Encrypt sensitive data before storing it locally.
  • Data Management: Implement proper data migration strategies when updating the database schema.
  • Asynchronous Operations: Use asynchronous operations to prevent blocking the main thread and ensure UI responsiveness.
  • Error Handling: Handle potential errors during data storage and retrieval to provide a smooth user experience.

Conclusion

Local data persistence is a critical aspect of Flutter development, enabling offline capabilities, improving performance, and enhancing the user experience. By understanding and implementing the appropriate methods, you can build robust and efficient Flutter applications. Whether you choose Shared Preferences, SQLite, Hive, or Isar, ensure that you follow best practices to maintain data integrity and security.