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
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.