Implementing Local Data Persistence in Flutter Apps

In Flutter, persisting data locally is essential for creating responsive and functional applications, especially when dealing with user preferences, cached data, or offline capabilities. Flutter offers several options for local data persistence, each with its own strengths and use cases. This article explores various methods for implementing local data persistence in Flutter, complete with code examples.

Why Local Data Persistence?

Local data persistence allows apps to store data on the device for later use, enabling:

  • Offline Functionality: Access data without an internet connection.
  • Faster Load Times: Retrieve stored data quickly from local storage.
  • User Preferences: Save user-specific settings for a personalized experience.
  • Data Caching: Store API responses to reduce network requests.

Methods for Local Data Persistence in Flutter

  1. Shared Preferences: For simple key-value pairs.
  2. SQLite: For structured data with relationships.
  3. Files: For storing any type of file.
  4. Hive: For a NoSQL key-value database.

1. Shared Preferences

Shared Preferences is suitable for storing small amounts of primitive data such as boolean, int, double, and string values. It’s simple and quick, making it ideal for storing user preferences or app settings.

Step 1: Add Dependency

Include the shared_preferences package in your pubspec.yaml file:

dependencies:
  shared_preferences: ^2.2.2

Run flutter pub get to install the dependency.

Step 2: Storing Data

Here’s how to store data using Shared Preferences:

import 'package:shared_preferences/shared_preferences.dart';

Future saveData(String key, String value) async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  await prefs.setString(key, value);
}
Step 3: Retrieving Data

To retrieve the stored data, use:

Future getData(String key) async {
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  return prefs.getString(key);
}
Step 4: Usage Example

An example of using Shared Preferences in a Flutter app:

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

class SharedPreferencesExample extends StatefulWidget {
  @override
  _SharedPreferencesExampleState createState() => _SharedPreferencesExampleState();
}

class _SharedPreferencesExampleState extends State {
  final _textController = TextEditingController();
  String _savedText = '';

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

  Future _loadSavedText() async {
    final prefs = await SharedPreferences.getInstance();
    setState(() {
      _savedText = prefs.getString('myText') ?? '';
      _textController.text = _savedText;
    });
  }

  Future _saveText(String text) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('myText', text);
    setState(() {
      _savedText = text;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Shared Preferences Example'),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: [
            TextField(
              controller: _textController,
              decoration: InputDecoration(
                labelText: 'Enter text',
                border: OutlineInputBorder(),
              ),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                _saveText(_textController.text);
              },
              child: Text('Save Text'),
            ),
            SizedBox(height: 20),
            Text('Saved Text: $_savedText'),
          ],
        ),
      ),
    );
  }
}

2. SQLite

SQLite is a powerful, lightweight database engine that allows you to store structured data in tables. It’s ideal for more complex data persistence needs.

Step 1: Add Dependency

Include the sqflite and path_provider packages in your pubspec.yaml file:

dependencies:
  sqflite: ^2.3.2
  path_provider: ^2.0.1

Run flutter pub get to install the dependencies.

Step 2: Create Database Helper

Implement a database helper class to manage database operations:

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

class DatabaseHelper {
  static const _databaseName = "MyDatabase.db";
  static const _databaseVersion = 1;

  static const table = 'my_table';

  static const columnId = '_id';
  static const columnName = 'name';
  static const columnAge = 'age';

  // Singleton instance
  DatabaseHelper._privateConstructor();
  static final DatabaseHelper instance = DatabaseHelper._privateConstructor();

  static Database? _database;

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

  Future _initDatabase() async {
    Directory documentsDirectory = await getApplicationDocumentsDirectory();
    String path = join(documentsDirectory.path, _databaseName);
    return await openDatabase(path,
        version: _databaseVersion, onCreate: _onCreate);
  }

  Future _onCreate(Database db, int version) async {
    await db.execute('''
      CREATE TABLE $table (
        $columnId INTEGER PRIMARY KEY,
        $columnName TEXT NOT NULL,
        $columnAge INTEGER NOT NULL
      )
      ''');
  }

  Future insert(Map row) async {
    Database db = await instance.database;
    return await db.insert(table, row);
  }

  Future>> queryAll() async {
    Database db = await instance.database;
    return await db.query(table);
  }
}
Step 3: Usage Example

An example of using SQLite in a Flutter app:

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

class SQLiteExample extends StatefulWidget {
  @override
  _SQLiteExampleState createState() => _SQLiteExampleState();
}

class _SQLiteExampleState extends State {
  final dbHelper = DatabaseHelper.instance;
  List> _records = [];

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

  void _insert() async {
    Map row = {
      DatabaseHelper.columnName: 'John Doe',
      DatabaseHelper.columnAge: 30
    };
    final id = await dbHelper.insert(row);
    print('inserted row id: $id');
    _queryAll();
  }

  void _queryAll() async {
    final allRows = await dbHelper.queryAll();
    setState(() {
      _records = allRows;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('SQLite Example'),
      ),
      body: Column(
        children: [
          ElevatedButton(
            onPressed: _insert,
            child: Text('Insert'),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _records.length,
              itemBuilder: (context, index) {
                return Card(
                  child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text('ID: ${_records[index][DatabaseHelper.columnId]}'),
                        Text('Name: ${_records[index][DatabaseHelper.columnName]}'),
                        Text('Age: ${_records[index][DatabaseHelper.columnAge]}'),
                      ],
                    ),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

3. Files

Storing data in files is suitable for saving complex data structures or any type of data that needs to be preserved as-is, like images, JSON files, or custom data formats.

Step 1: Add Dependency

Include the path_provider package in your pubspec.yaml file:

dependencies:
  path_provider: ^2.0.1

Run flutter pub get to install the dependency.

Step 2: Writing to a File

Here’s how to write data to a file:

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

Future getLocalFile(String filename) async {
  final directory = await getApplicationDocumentsDirectory();
  return File('${directory.path}/$filename');
}

Future writeFile(String filename, String data) async {
  final file = await getLocalFile(filename);
  return file.writeAsString(data);
}
Step 3: Reading from a File

To read data from the file, use:

Future readFile(String filename) async {
  try {
    final file = await getLocalFile(filename);
    final contents = await file.readAsString();
    return contents;
  } catch (e) {
    return '';
  }
}
Step 4: Usage Example

An example of using file storage in a Flutter app:

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

class FileStorageExample extends StatefulWidget {
  @override
  _FileStorageExampleState createState() => _FileStorageExampleState();
}

class _FileStorageExampleState extends State {
  final _textController = TextEditingController();
  String _fileContent = '';
  final String _fileName = 'my_data.txt';

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

  Future _loadDataFromFile() async {
    try {
      final directory = await getApplicationDocumentsDirectory();
      final file = File('${directory.path}/$_fileName');
      if (await file.exists()) {
        _fileContent = await file.readAsString();
      } else {
        _fileContent = 'File does not exist.';
      }
    } catch (e) {
      _fileContent = 'Error loading data: $e';
    }
    setState(() {});
  }

  Future _saveDataToFile(String data) async {
    try {
      final directory = await getApplicationDocumentsDirectory();
      final file = File('${directory.path}/$_fileName');
      await file.writeAsString(data);
      setState(() {
        _fileContent = 'Data saved successfully.';
      });
    } catch (e) {
      setState(() {
        _fileContent = 'Error saving data: $e';
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('File Storage Example'),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: [
            TextField(
              controller: _textController,
              decoration: InputDecoration(
                labelText: 'Enter text to save',
                border: OutlineInputBorder(),
              ),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                _saveDataToFile(_textController.text);
              },
              child: Text('Save to File'),
            ),
            SizedBox(height: 20),
            Text('File Content: $_fileContent'),
          ],
        ),
      ),
    );
  }
}

4. Hive

Hive is a lightweight NoSQL database that is efficient and easy to use in Flutter apps. It’s perfect for more structured data than Shared Preferences but without the overhead of SQLite.

Step 1: Add Dependency

Include the hive and hive_flutter packages in your pubspec.yaml file:

dependencies:
  hive: ^2.2.3
  hive_flutter: ^1.1.0

dev_dependencies:
  hive_generator: ^2.0.1
  build_runner: ^2.4.8

Also add required dependencies. Remember to run flutter pub run build_runner build whenever you update your hive object class:

dependencies:
  json_annotation: ^4.8.1
dev_dependencies:
  build_runner: ^2.4.8
  json_serializable: ^6.6.1

Run flutter pub get to install the dependencies.

Step 2: Define a Hive Object

To use Hive, you need to define a Hive object using annotations. This will be how you manage more complex data in key value pairs


import 'package:hive/hive.dart';
import 'package:json_annotation/json_annotation.dart';

part 'person.g.dart';

@HiveType(typeId: 0)
@JsonSerializable()
class Person {
  @HiveField(0)
  String name;

  @HiveField(1)
  int age;

  Person({required this.name, required this.age});

  /// A necessary factory constructor for creating a new Person
  /// instance from a map. Pass the map to the base class.
  factory Person.fromJson(Map json) => _$PersonFromJson(json);

  /// `toJson` is the convention for objects to support serialization
  /// to JSON. The implementation simply calls the private, generated
  /// helper method `_$PersonToJson`.
  Map toJson() => _$PersonToJson(this);
}

Run flutter pub run build_runner build

Step 3: Initialize Hive

Initialize Hive in your main function:


import 'package:flutter/widgets.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:persistance_example/person.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize Hive
  await Hive.initFlutter('hive_db');
  // Register Hive Adapters
  Hive.registerAdapter(PersonAdapter());
  // Open the boxes
  await Hive.openBox('peopleBox');

  runApp(MyApp());
}
Step 4: CRUD operation to Data

The CRUD Operation could be the insert, read, update, delete Data with unique ID. Follow the pattern that how easy use


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

class HiveStorageExample extends StatefulWidget {
  @override
  _HiveStorageExampleState createState() => _HiveStorageExampleState();
}

class _HiveStorageExampleState extends State {
  final _nameController = TextEditingController();
  final _ageController = TextEditingController();

  late Box _peopleBox;

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

  Future _openBox() async {
    _peopleBox = Hive.box('peopleBox');
  }

  void _addPerson() {
    final name = _nameController.text;
    final age = int.tryParse(_ageController.text) ?? 0;
    
    final person = Person(name: name, age: age);
    _peopleBox.add(person);
    
    _nameController.clear();
    _ageController.clear();
    
    print('Added ${person.name} to Hive');
  }

  void _deletePerson(int index) {
    _peopleBox.deleteAt(index);
    print('Deleted person at index $index');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Hive Storage Example'),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: [
            TextField(
              controller: _nameController,
              decoration: InputDecoration(
                labelText: 'Enter name',
                border: OutlineInputBorder(),
              ),
            ),
            TextField(
              controller: _ageController,
              decoration: InputDecoration(
                labelText: 'Enter age',
                border: OutlineInputBorder(),
              ),
              keyboardType: TextInputType.number,
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: _addPerson,
              child: Text('Add Person'),
            ),
            Expanded(
              child: ValueListenableBuilder(
                valueListenable: _peopleBox.listenable(),
                builder: (context, Box box, _) {
                  if (box.isEmpty) {
                    return Center(child: Text('No data'));
                  } else {
                    return ListView.builder(
                      itemCount: box.length,
                      itemBuilder: (context, index) {
                        final person = box.getAt(index);
                        return Card(
                          child: Padding(
                            padding: const EdgeInsets.all(8.0),
                            child: Row(
                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
                              children: [
                                Column(
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: [
                                    Text('Name: ${person?.name}'),
                                    Text('Age: ${person?.age}'),
                                  ],
                                ),
                                IconButton(
                                  icon: Icon(Icons.delete),
                                  onPressed: () => _deletePerson(index),
                                )
                              ],
                            ),
                          ),
                        );
                      },
                    );
                  }
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Choosing the Right Persistence Method

  • Shared Preferences: Simple settings and small data (e.g., user preferences).
  • SQLite: Structured and relational data (e.g., local database for a task manager app).
  • Files: Complex data structures and large files (e.g., images, JSON data).
  • Hive: Efficient, lightweight database for more complex structured data (e.g., caching API responses).

Conclusion

Implementing local data persistence is crucial for creating robust and user-friendly Flutter applications. By using methods like Shared Preferences, SQLite, file storage, and Hive, developers can effectively manage different types of data and provide a seamless user experience, even when offline.