Flutter Data Persistence

5 mins

5 mins

Ashutosh

Published on Oct 18, 2024

Flutter Data Persistence: Store Key-Value Data with SharedPreferences

Introduction

When building mobile apps, managing data efficiently is key to ensuring a smooth user experience. Flutter, as a popular cross-platform framework, offers various options for data persistence. Understanding how to persist data is crucial for apps that handle user preferences, settings, or large datasets that must be retained across sessions.

In this blog, we will explore the three most common data persistence methods in Flutter: key-value storage, file storage, and SQLite database. We will explain how to implement these techniques, provide sample code, and compare their use cases. By the end, you’ll understand which method is best for your app, whether for storing simple user preferences or handling complex data structures.

In our previous blogs, we explored the fundamentals of JSON serialization in Flutter and how to efficiently parse JSON data to avoid performance issues. In JSON and Serialization in Flutter: A Comprehensive Guide for Flutter App Development, we discussed various methods for serializing data, while How to Parse JSON in the Background with Flutter: Parsing Large JSON Files to Avoid Jank focused on using background isolates to handle large JSON files without impacting UI performance.

Flutter Data Persistence

Data persistence refers to the ability of an app to store data even after the app has been closed or the device restarted. In Flutter, data can be persisted using different approaches:

  1. Key-Value Storage: Simple data storage using key-value pairs, usually for settings or small amounts of data.

  2. File Handling: Storing and reading data from files, is useful for more complex data storage needs.

  3. SQLite: A relational database that stores structured data, ideal for apps requiring the management of complex data.

Why Persistence is Essential for App Development

Persisting data is crucial because it allows users to pick up where they left off, provides consistency across sessions, and improves the overall user experience. Whether you are developing a to-do app, a social media platform, or an e-commerce app, data persistence ensures that the data remains intact between app uses.

Store key-value data on disk

One of the easiest ways to persist simple key-value data in Flutter is by using the shared_preferences plugin. This plugin is perfect for saving small amounts of data such as user preferences, settings, or counters.

Key-Value Storage with SharedPreferences

SharedPreferences allows you to store data as key-value pairs. This is particularly useful when you need to save user settings, authentication tokens, or any other small pieces of information. The plugin automatically handles the platform-specific storage mechanisms, allowing you to save data on both iOS and Android using a consistent API.

Steps to Use SharedPreferences

  1. Add the Dependency
    To begin, add the shared_preferences package to your project’s pubspec.yaml file:

    flutter pub add shared_preferences
  2. Saving Data
    Use the setter methods provided by the SharedPreferences class to store data. These methods, such as setInt, setBool, and setString, allow you to store different types of primitive data.

    final prefs = await SharedPreferences.getInstance();
    await prefs.setInt('counter', counter);
  3. Reading Data
    To retrieve the stored data, use the corresponding getter methods, such as getInt, getBool, and getString. If the data is not found, you can provide a default value.

    final counter = prefs.getInt('counter') ?? 0;
  4. Removing Data
    To delete a specific key-value pair, use the remove() method:

    await prefs.remove('counter');

Supported Data Types and Limitations

SharedPreferences supports primitive data types, including int, double, bool, String, and List<String>. However, it is not designed to handle complex or large datasets. It should be used for small amounts of simple data only, as it is not suitable for handling large data files.

Example Code for Saving and Retrieving Data

final prefs = await SharedPreferences.getInstance();
await prefs.setInt('counter', 42);

final counter = prefs.getInt('counter') ?? 0;

Testing SharedPreferences with Mock Data

The shared_preferences package provides a mock implementation, making it easier to test your code. You can mock key-value pairs in memory during testing by calling SharedPreferences.setMockInitialValues.

Reading and Writing Files in Flutter

For more complex data storage, such as saving files, Flutter allows you to store and read data from files using the path_provider plugin. File storage is useful when you need to save large amounts of data, configurations, or media files.

Introduction to File Storage in Flutter

With file storage, you can save data to specific directories on the device. This method is ideal for storing user-generated content, app logs, or configuration files.

Steps to Use File Storage in Flutter

  1. Add the Dependency
    Add the path_provider plugin to access device directories:

    flutter pub add path_provider
  2. Writing Data to a File
    Use the writeAsString() method to write data to a file:

    final directory = await getApplicationDocumentsDirectory();
    final file = File('${directory.path}/my_file.txt');
    await file.writeAsString('Flutter is awesome!');
  3. Reading Data from a File
    Use the readAsString() method to retrieve the stored data:

    final directory = await getApplicationDocumentsDirectory();
    final file = File('${directory.path}/my_file.txt');
    final contents = await file.readAsString();

Example Code: Saving and Retrieving Text Data

Future<void> writeToFile(String text) async {
  final directory = await getApplicationDocumentsDirectory();
  final file = File('${directory.path}/my_file.txt');
  await file.writeAsString(text);
}

Future<String> readFromFile() async {
  final directory = await getApplicationDocumentsDirectory();
  final file = File('${directory.path}/my_file.txt');
  return await file.readAsString();
}

When to Use File Storage

File storage is more appropriate than SharedPreferences when you need to handle larger amounts of data or non-primitive types. For example, it’s a great option for storing user-generated content, documents, or large text files.

Persisting Data with SQLite in Flutter

SQLite is a powerful relational database that is well-suited for apps requiring complex data handling. It supports structured data with relationships, making it ideal for apps that require more than basic key-value storage.

Introduction to SQLite

SQLite is a lightweight, SQL-based database engine that runs on the device. It provides a robust way to store structured data in tables, perform complex queries, and handle relational data efficiently.

Steps to Use SQLite in Flutter

  1. Add the Dependencies
    To use SQLite in Flutter, add the sqflite and path_provider dependencies:

    flutter pub add sqflite
    flutter pub add path_provider
  2. Creating a SQLite Database
    First, create the database and define your table structure:

    final database = openDatabase(
      join(await getDatabasesPath(), 'my_database.db'),
      onCreate: (db, version) {
        return db.execute(
          "CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)",
        );
      },
      version: 1,
    );
  3. Performing CRUD Operations
    You can perform Create, Read, Update, and Delete (CRUD) operations using SQLite. Here’s how to insert a record:

    Future<void> insertUser(User user) async {
      final db = await database;
      await db.insert(
        'users',
        user.toMap(),
        conflictAlgorithm: ConflictAlgorithm.replace,
      );
    }

Example Code: Building a Small Database App

class User {
  final int id;
  final String name;
  final int age;

  User({required this.id, required this.name, required this.age});

  Map<String, dynamic> toMap() {
    return {'id': id, 'name': name, 'age': age};
  }
}

Advantages of SQLite for Persistent Data Management

SQLite is ideal for apps that require structured data, multiple records, and relationships between data points. It provides powerful querying capabilities and ensures data is efficiently managed.

Complete example

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

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Shared preferences demo',
      home: MyHomePage(title: 'Shared preferences demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

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

  /// Load the initial counter value from persistent storage on start,
  /// or fallback to 0 if it doesn't exist.
  Future<void> _loadCounter() async {
    final prefs = await SharedPreferences.getInstance();
    setState(() {
      _counter = prefs.getInt('counter') ?? 0;
    });
  }

  /// After a click, increment the counter state and
  /// asynchronously save it to persistent storage.
  Future<void> _incrementCounter() async {
    final prefs = await SharedPreferences.getInstance();
    setState(() {
      _counter = (prefs.getInt('counter') ?? 0) + 1;
      prefs.setInt('counter', _counter);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              'You have pushed the button this many times: ',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

When to Use Key-Value Storage, Files, or SQLite

Each data persistence method has its use case:

  • Key-Value Storage (SharedPreferences): Use for small, simple data like user preferences or authentication tokens.

  • File Storage: Best for saving larger data like configuration files, documents, or media files.

  • SQLite: Ideal for apps that require structured data, such as databases of users, transactions, or inventory.

Best Practices and Recommendations

  • Always choose the simplest method that meets your needs.

  • Avoid using SharedPreferences for large data sets.

  • Use SQLite for complex data structures and relationships.

Conclusion

Data persistence in Flutter is essential for building robust, user-friendly apps. Depending on your app’s requirements, you can choose between key-value storage, file storage, or SQLite. By mastering these techniques, you can ensure your app is efficient, responsive, and provides a seamless user experience.

Explore these methods, practice using them in your projects, and take the next step in becoming a proficient Flutter developer. Don't forget to join the Flutter community and stay updated with the latest Flutter news and tutorials!