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
- Shared Preferences: For simple key-value pairs.
- SQLite: For structured data with relationships.
- Files: For storing any type of file.
- 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
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.