Implementing State Persistence Across Application Sessions in Flutter

State management is a crucial aspect of modern app development. In Flutter, it becomes even more critical when you need to maintain the state of your application across different sessions. Whether it’s user preferences, shopping cart data, or the last screen visited, persisting application state ensures a seamless user experience. In this blog post, we will explore various techniques for implementing state persistence across application sessions in Flutter.

Why is State Persistence Important?

State persistence enhances the user experience by:

  • Remembering user preferences, so users don’t have to configure settings every time they open the app.
  • Preserving the user’s progress in the app, such as the current step in a form or the items in a shopping cart.
  • Providing a consistent and personalized experience, which can improve user engagement and satisfaction.

Techniques for State Persistence in Flutter

There are several methods for implementing state persistence in Flutter, each with its own strengths and use cases. Here are some of the most common techniques:

1. Using shared_preferences

shared_preferences is a simple and widely-used plugin for persisting key-value pairs on disk. It’s suitable for storing small amounts of primitive data like user settings or flags.

Step 1: Add the shared_preferences Dependency

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

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.2.2

Then, run flutter pub get to install the dependency.

Step 2: Persist and Retrieve Data

Here’s an example of how to use shared_preferences to persist and retrieve data:

import 'package:shared_preferences/shared_preferences.dart';

class AppPreferences {
  static const String _keyTheme = 'theme';

  static Future setTheme(String theme) async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setString(_keyTheme, theme);
  }

  static Future getTheme() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    return prefs.getString(_keyTheme);
  }
}
Step 3: Usage in Your App

You can use the AppPreferences class to save and load the theme setting:

import 'package:flutter/material.dart';

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State {
  String _theme = 'light';

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

  Future _loadTheme() async {
    final theme = await AppPreferences.getTheme();
    setState(() {
      _theme = theme ?? 'light';
    });
  }

  Future _setTheme(String theme) async {
    await AppPreferences.setTheme(theme);
    setState(() {
      _theme = theme;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: _theme == 'light' ? ThemeData.light() : ThemeData.dark(),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Theme Demo'),
        ),
        body: Center(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                onPressed: () => _setTheme('light'),
                child: const Text('Light Theme'),
              ),
              const SizedBox(width: 20),
              ElevatedButton(
                onPressed: () => _setTheme('dark'),
                child: const Text('Dark Theme'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

2. Using SQLite Database

For more structured data persistence, consider using SQLite. Flutter provides excellent support for SQLite through the sqflite package. This is suitable for managing larger, more complex data sets.

Step 1: Add the sqflite Dependency

Include sqflite in your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  sqflite: ^2.3.0
  path_provider: ^2.2.0

Run flutter pub get.

Step 2: Define the Database Helper

Create a helper class to manage the database connection and operations:

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

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

  static const table = 'my_table';
  static const columnId = '_id';
  static const columnTitle = 'title';
  static const columnDone = 'done';

  // Make this a singleton class
  DatabaseHelper._privateConstructor();
  static final DatabaseHelper instance = DatabaseHelper._privateConstructor();

  // Only have a single app-wide reference to the database
  static Database? _database;
  Future get database async {
    if (_database != null) return _database!;
    // Lazily instantiate the db the first time it is accessed
    _database = await _initDatabase();
    return _database!;
  }

  // This opens (and creates if it doesn't exist) the database
  Future _initDatabase() async {
    final documentsDirectory = await getApplicationDocumentsDirectory();
    final path = join(documentsDirectory.path, _databaseName);
    return await openDatabase(
      path,
      version: _databaseVersion,
      onCreate: _onCreate,
    );
  }

  // SQL code to create the database table
  Future _onCreate(Database db, int version) async {
    await db.execute('''
      CREATE TABLE $table (
        $columnId INTEGER PRIMARY KEY,
        $columnTitle TEXT NOT NULL,
        $columnDone INTEGER NOT NULL
      )
      ''');
  }

  // Helper methods

  // Inserts a row in the database where each key in the Map is a column name
  // and the value is the column value. The return value is the id of the
  // inserted row.
  Future insert(Map row) async {
    final db = await instance.database;
    return await db.insert(table, row);
  }

  // All of the rows are returned as a list of maps, where each map is
  // a key-value list of columns.
  Future>> queryAllRows() async {
    final db = await instance.database;
    return await db.query(table);
  }
}
Step 3: Usage in Your App

Use the DatabaseHelper to perform CRUD operations:

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

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State {
  final dbHelper = DatabaseHelper.instance;
  List> _items = [];

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

  Future _loadItems() async {
    final allRows = await dbHelper.queryAllRows();
    setState(() {
      _items = allRows;
    });
  }

  Future _insertItem(String title) async {
    Map row = {
      DatabaseHelper.columnTitle: title,
      DatabaseHelper.columnDone: 0,
    };
    await dbHelper.insert(row);
    _loadItems();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('SQLite Demo'),
        ),
        body: ListView.builder(
          itemCount: _items.length,
          itemBuilder: (context, index) {
            return ListTile(
              title: Text(_items[index][DatabaseHelper.columnTitle]),
            );
          },
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () => _insertItem('New Item'),
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}

3. Using Hive Database

Hive is a lightweight NoSQL database that provides excellent performance and simplicity. It is a great alternative to SQLite for more complex data that does not require SQL-like queries.

Step 1: Add Hive Dependencies

Add the required dependencies in pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  hive: ^2.2.3
  hive_flutter: ^1.1.2

dev_dependencies:
  hive_generator: ^2.0.1
  build_runner: ^2.4.6

Run flutter pub get and then flutter pub run build_runner build.

Step 2: Define Hive Model and Adapter

Create a model class for your data and a Hive adapter for serialization:

import 'package:hive/hive.dart';

part 'item.g.dart';

@HiveType(typeId: 0)
class Item {
  @HiveField(0)
  String title;

  @HiveField(1)
  bool done;

  Item({required this.title, required this.done});
}
Step 3: Initialize Hive

Initialize Hive in your main function:

import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'item.dart';
import 'app.dart'; // Your main app widget

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Hive.initFlutter();
  Hive.registerAdapter(ItemAdapter());
  await Hive.openBox('items');
  runApp(MyApp());
}
Step 4: Usage in Your App

Use Hive to store and retrieve data:

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

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Hive Demo'),
        ),
        body: ValueListenableBuilder>(
          valueListenable: Hive.box('items').listenable(),
          builder: (context, box, _) {
            final items = box.values.toList().cast();
            return ListView.builder(
              itemCount: items.length,
              itemBuilder: (context, index) {
                final item = items[index];
                return ListTile(
                  title: Text(item.title),
                );
              },
            );
          },
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            final box = Hive.box('items');
            box.add(Item(title: 'New Item', done: false));
          },
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}

4. Using Flutter Secure Storage

For sensitive data, such as user tokens or passwords, it is important to use secure storage. The flutter_secure_storage package provides a secure way to store key-value pairs.

Step 1: Add the flutter_secure_storage Dependency

Include flutter_secure_storage in your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  flutter_secure_storage: ^9.0.0

Run flutter pub get.

Step 2: Store and Retrieve Data Securely
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class SecureStorage {
  final _storage = const FlutterSecureStorage();

  Future setToken(String token) async {
    await _storage.write(key: 'auth_token', value: token);
  }

  Future getToken() async {
    return await _storage.read(key: 'auth_token');
  }

  Future deleteToken() async {
    await _storage.delete(key: 'auth_token');
  }
}
Step 3: Usage in Your App
import 'package:flutter/material.dart';

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State {
  final _secureStorage = SecureStorage();
  String _token = 'No Token';

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

  Future _loadToken() async {
    final token = await _secureStorage.getToken();
    setState(() {
      _token = token ?? 'No Token';
    });
  }

  Future _saveToken(String token) async {
    await _secureStorage.setToken(token);
    _loadToken();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Secure Storage Demo'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('Token: $_token'),
              ElevatedButton(
                onPressed: () => _saveToken('My Secret Token'),
                child: const Text('Save Token'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Choosing the Right Technique

Selecting the appropriate method depends on several factors:

  • Data Size: shared_preferences is suitable for small data sets, while SQLite and Hive are better for larger ones.
  • Data Complexity: Use SQLite or Hive for structured data and shared_preferences for simple key-value pairs.
  • Security Needs: Employ flutter_secure_storage for sensitive information.

Conclusion

State persistence across application sessions is vital for delivering a superior user experience in Flutter apps. Whether through shared_preferences for basic settings, SQLite or Hive for more structured data, or flutter_secure_storage for sensitive data, Flutter provides robust tools to manage and persist application state effectively. By leveraging these techniques, you can create applications that are both user-friendly and reliable.