Table of Contents
Introduction to Local Storage in Flutter
Using shared_preferences for Simple Key-Value Storage
2.1 What is shared_preferences?
2.2 Setting Up shared_preferences
2.3 Real-Life Example: Saving User Settings
2.4 Best Practices and Exception Handling
2.5 Pros and Cons of shared_preferences
2.6 Alternatives to shared_preferences
Introduction to SQLite and sqflite
3.1 What is SQLite?
3.2 Setting Up sqflite in Flutter
3.3 Database Schema Design
CRUD Operations in SQLite
4.1 Creating a Database and Table
4.2 Inserting Data (Create)
4.3 Reading Data (Read)
4.4 Updating Data (Update)
4.5 Deleting Data (Delete)
4.6 Exception Handling in SQLite
4.7 Pros and Cons of SQLite
4.8 Alternatives to SQLite
File Storage for Images and Assets
5.1 Understanding File Storage in Flutter
5.2 Saving and Retrieving Files
5.3 Real-Life Example: Storing User Profile Pictures
5.4 Best Practices and Exception Handling
5.5 Pros and Cons of File Storage
5.6 Alternatives to File Storage
Managing App Data Lifecycle
6.1 Data Persistence Strategies
6.2 Handling Data Migration
6.3 Backup and Restore
Practical Exercise: Extending a To-Do List App with SQLite
7.1 Project Overview
7.2 Step-by-Step Implementation
7.3 Testing and Debugging
7.4 Enhancing the App with Advanced Features
Conclusion and Next Steps
1. Introduction to Local Storage in Flutter
Local storage is critical for mobile apps to provide offline functionality, persist user preferences, and manage complex data. In Flutter, local storage can be achieved using multiple methods, such as shared_preferences for simple key-value pairs, SQLite for structured data, and file storage for assets like images. This module focuses on implementing these techniques, with a practical emphasis on building a to-do list app that saves tasks locally using SQLite.
Local storage ensures apps remain functional without an internet connection, improves performance by reducing server calls, and enhances user experience by retaining data across sessions. We’ll explore beginner-friendly to advanced scenarios, with real-life examples, best practices, and exception handling.
2. Using shared_preferences for Simple Key-Value Storage
2.1 What is shared_preferences?
shared_preferences is a Flutter plugin for storing simple key-value pairs persistently on a device. It’s ideal for lightweight data like user settings, app preferences, or small flags (e.g., dark mode toggle, user name). Data is stored in a platform-specific way: SharedPreferences on Android and NSUserDefaults on iOS.
2.2 Setting Up shared_preferences
Add the shared_preferences package to your pubspec.yaml:
dependencies:
shared_preferences: ^2.2.3
Run flutter pub get to install the package.
2.3 Real-Life Example: Saving User Settings
Imagine an app where users can toggle dark mode and save their username. Here’s how to implement this using shared_preferences.
Step 1: Initialize shared_preferences
import 'package:shared_preferences/shared_preferences.dart';
Future<void> saveUserSettings(String username, bool isDarkMode) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('username', username);
await prefs.setBool('isDarkMode', isDarkMode);
}
Future<Map<String, dynamic>> loadUserSettings() async {
final prefs = await SharedPreferences.getInstance();
final username = prefs.getString('username') ?? 'Guest';
final isDarkMode = prefs.getBool('isDarkMode') ?? false;
return {'username': username, 'isDarkMode': isDarkMode};
}
Step 2: Build a Settings Screen
Create a UI to save and load settings:
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
runApp(const SettingsApp());
}
class SettingsApp extends StatelessWidget {
const SettingsApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
home: const SettingsScreen(),
);
}
}
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
_SettingsScreenState createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
final TextEditingController _controller = TextEditingController();
bool _isDarkMode = false;
String _username = 'Guest';
@override
void initState() {
super.initState();
_loadSettings();
}
Future<void> _loadSettings() async {
final settings = await loadUserSettings();
setState(() {
_username = settings['username'];
_isDarkMode = settings['isDarkMode'];
_controller.text = _username;
});
}
Future<void> _saveSettings() async {
await saveUserSettings(_controller.text, _isDarkMode);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Settings saved!')),
);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: _isDarkMode ? ThemeData.dark() : ThemeData.light(),
home: Scaffold(
appBar: AppBar(title: const Text('User Settings')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _controller,
decoration: const InputDecoration(labelText: 'Username'),
),
SwitchListTile(
title: const Text('Dark Mode'),
value: _isDarkMode,
onChanged: (value) {
setState(() {
_isDarkMode = value;
});
},
),
ElevatedButton(
onPressed: _saveSettings,
child: const Text('Save Settings'),
),
Text('Current User: $_username'),
],
),
),
),
);
}
}
Explanation:
Saving: The saveUserSettings function stores the username and dark mode preference.
Loading: The loadUserSettings function retrieves the saved data, with defaults (Guest, false) if no data exists.
UI: A TextField captures the username, a SwitchListTile toggles dark mode, and a button saves the settings. The app updates the theme dynamically.
2.4 Best Practices and Exception Handling
Asynchronous Operations: Always use await when accessing SharedPreferences to avoid blocking the UI.
Error Handling:
Future<void> saveUserSettings(String username, bool isDarkMode) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString('username', username); await prefs.setBool('isDarkMode', isDarkMode); } catch (e) { print('Error saving settings: $e'); // Handle error (e.g., show user a message) } }
Data Validation: Validate input before saving (e.g., ensure username isn’t empty).
Clearing Data: Use prefs.clear() to reset preferences when needed (e.g., user logout).
2.5 Pros and Cons of shared_preferences
Pros:
Simple and lightweight for small data.
Platform-agnostic (works on iOS and Android).
Easy to integrate and use.
Cons:
Limited to basic data types (int, double, bool, String, List).
Not suitable for large or structured data.
No query support or complex data relationships.
2.6 Alternatives to shared_preferences
Hive: A lightweight, NoSQL key-value store for more complex data.
Secure Storage: flutter_secure_storage for sensitive data like tokens or passwords.
File Storage: For larger, unstructured data (covered later).
3. Introduction to SQLite and sqflite
3.1 What is SQLite?
SQLite is a lightweight, serverless, relational database engine that stores data locally on the device. It’s ideal for structured data like user profiles, tasks, or transactions. The sqflite package is Flutter’s go-to solution for SQLite integration.
3.2 Setting Up sqflite in Flutter
Add sqflite and path to pubspec.yaml:
dependencies:
sqflite: ^2.3.0
path: ^1.8.3
Run flutter pub get.
3.3 Database Schema Design
For a to-do list app, design a table tasks with columns:
id: Unique identifier (auto-incremented).
title: Task description.
isCompleted: Boolean for task status.
createdAt: Timestamp for creation.
4. CRUD Operations in SQLite
4.1 Creating a Database and Table
Create a DatabaseHelper class to manage the database:
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
static final DatabaseHelper instance = DatabaseHelper._init();
static Database? _database;
DatabaseHelper._init();
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB('tasks.db');
return _database!;
}
Future<Database> _initDB(String fileName) async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, fileName);
return await openDatabase(path, version: 1, onCreate: _createDB);
}
Future _createDB(Database db, int version) async {
await db.execute('''
CREATE TABLE tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
isCompleted INTEGER NOT NULL,
createdAt TEXT NOT NULL
)
''');
}
Future close() async {
final db = await database;
db.close();
}
}
Explanation:
Singleton Pattern: Ensures a single database instance.
Database Path: Uses path package to construct a platform-appropriate file path.
Table Creation: Defines the tasks table with id, title, isCompleted, and createdAt.
4.2 Inserting Data (Create)
Add a method to insert a task:
class Task {
final int? id;
final String title;
final bool isCompleted;
final DateTime createdAt;
Task({
this.id,
required this.title,
this.isCompleted = false,
required this.createdAt,
});
Map<String, dynamic> toMap() {
return {
'id': id,
'title': title,
'isCompleted': isCompleted ? 1 : 0,
'createdAt': createdAt.toIso8601String(),
};
}
factory Task.fromMap(Map<String, dynamic> map) {
return Task(
id: map['id'],
title: map['title'],
isCompleted: map['isCompleted'] == 1,
createdAt: DateTime.parse(map['createdAt']),
);
}
}
Future<void> insertTask(Task task) async {
final db = await DatabaseHelper.instance.database;
await db.insert('tasks', task.toMap(), conflictAlgorithm: ConflictAlgorithm.replace);
}
4.3 Reading Data (Read)
Retrieve all tasks or a specific task:
Future<List<Task>> getTasks() async {
final db = await DatabaseHelper.instance.database;
final maps = await db.query('tasks', orderBy: 'createdAt DESC');
return List.generate(maps.length, (i) => Task.fromMap(maps[i]));
}
Future<Task?> getTaskById(int id) async {
final db = await DatabaseHelper.instance.database;
final maps = await db.query('tasks', where: 'id = ?', whereArgs: [id]);
if (maps.isNotEmpty) return Task.fromMap(maps.first);
return null;
}
4.4 Updating Data (Update)
Update a task’s details:
Future<void> updateTask(Task task) async {
final db = await DatabaseHelper.instance.database;
await db.update('tasks', task.toMap(), where: 'id = ?', whereArgs: [task.id]);
}
4.5 Deleting Data (Delete)
Delete a task:
Future<void> deleteTask(int id) async {
final db = await DatabaseHelper.instance.database;
await db.delete('tasks', where: 'id = ?', whereArgs: [id]);
}
4.6 Exception Handling in SQLite
Handle database errors gracefully:
Future<void> insertTask(Task task) async {
try {
final db = await DatabaseHelper.instance.database;
await db.insert('tasks', task.toMap(), conflictAlgorithm: ConflictAlgorithm.replace);
} catch (e) {
print('Error inserting task: $e');
// Notify user or retry
}
}
4.7 Pros and Cons of SQLite
Pros:
Supports complex queries and relational data.
Lightweight and serverless.
Suitable for structured data with relationships.
Cons:
Requires schema management.
Not ideal for large datasets or high-concurrency apps.
Manual SQL query writing can be error-prone.
4.8 Alternatives to SQLite
Hive: Fast, lightweight NoSQL database.
ObjectBox: High-performance NoSQL database with Flutter support.
Firebase Firestore: For cloud-based storage with offline sync (if server-side is an option).
5. File Storage for Images and Assets
5.1 Understanding File Storage in Flutter
File storage is used for saving assets like images, videos, or JSON files. Flutter uses the path_provider package to access device directories (e.g., temporary or documents directory).
Add to pubspec.yaml:
dependencies:
path_provider: ^2.1.1
5.2 Saving and Retrieving Files
Save an image to the documents directory:
import 'dart:io';
import 'package:path_provider/path_provider.dart';
Future<File> saveImage(File image) async {
final directory = await getApplicationDocumentsDirectory();
final path = join(directory.path, 'profile_picture.jpg');
return image.copy(path);
}
Future<File?> getImage() async {
final directory = await getApplicationDocumentsDirectory();
final path = join(directory.path, 'profile_picture.jpg');
final file = File(path);
return file.existsSync() ? file : null;
}
5.3 Real-Life Example: Storing User Profile Pictures
For a social media app, allow users to upload and display a profile picture:
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key});
@override
_ProfileScreenState createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
File? _image;
@override
void initState() {
super.initState();
_loadImage();
}
Future<void> _loadImage() async {
final image = await getImage();
setState(() {
_image = image;
});
}
Future<void> _pickImage() async {
final picker = ImagePicker();
final pickedFile = await picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
final savedImage = await saveImage(File(pickedFile.path));
setState(() {
_image = savedImage;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Profile Picture')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_image == null
? const Text('No image selected.')
: Image.file(_image!, height: 200),
ElevatedButton(
onPressed: _pickImage,
child: const Text('Pick Image'),
),
],
),
),
);
}
}
Explanation:
Image Picker: Uses image_picker to select an image from the gallery.
Storage: Saves the image to the documents directory and retrieves it for display.
UI: Displays the image or a placeholder text if no image exists.
5.4 Best Practices and Exception Handling
Check Permissions: Ensure storage permissions are granted (use permission_handler).
Error Handling:
Future<File> saveImage(File image) async { try { final directory = await getApplicationDocumentsDirectory(); final path = join(directory.path, 'profile_picture.jpg'); return await image.copy(path); } catch (e) { print('Error saving image: $e'); rethrow; } }
File Size Management: Compress images to save space.
Clean Up: Delete unused files to prevent storage bloat.
5.5 Pros and Cons of File Storage
Pros:
Suitable for large, unstructured data (images, videos).
Simple to implement for assets.
Platform-agnostic with path_provider.
Cons:
No query support or data relationships.
Manual file management required.
Potential storage limitations on devices.
5.6 Alternatives to File Storage
Cloud Storage: Firebase Storage for online asset storage.
Cached Network Images: For temporary image storage with caching.
Database BLOBs: Store small binary data in SQLite or Hive.
6. Managing App Data Lifecycle
6.1 Data Persistence Strategies
Choose the right storage method based on data type:
Key-Value (shared_preferences): User settings, flags.
Database (SQLite): Structured data like tasks or profiles.
File Storage: Images, videos, or large files.
6.2 Handling Data Migration
When updating the database schema (e.g., adding a new column), use onUpgrade:
Future _createDB(Database db, int version) async {
await db.execute('''
CREATE TABLE tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
isCompleted INTEGER NOT NULL,
createdAt TEXT NOT NULL,
priority TEXT
)
''');
}
Future<Database> _initDB(String fileName) async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, fileName);
return await openDatabase(path, version: 2, onCreate: _createDB, onUpgrade: _onUpgrade);
}
Future _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion < 2) {
await db.execute('ALTER TABLE tasks ADD COLUMN priority TEXT');
}
}
6.3 Backup and Restore
To back up the database:
Future<void> backupDatabase() async {
final dbPath = await getDatabasesPath();
final dbFile = File(join(dbPath, 'tasks.db'));
final backupFile = File(join(dbPath, 'tasks_backup.db'));
await dbFile.copy(backupFile.path);
}
To restore:
Future<void> restoreDatabase() async {
final dbPath = await getDatabasesPath();
final backupFile = File(join(dbPath, 'tasks_backup.db'));
final dbFile = File(join(dbPath, 'tasks.db'));
if (await backupFile.exists()) {
await backupFile.copy(dbFile.path);
}
}
7. Practical Exercise: Extending a To-Do List App with SQLite
7.1 Project Overview
Build a to-do list app that allows users to create, read, update, and delete tasks, with data persisted using SQLite. The app includes a list view, task input form, and completion toggle.
7.2 Step-by-Step Implementation
Step 1: Set Up the Project
Create a new Flutter project and add dependencies:
dependencies:
flutter:
sdk: flutter
sqflite: ^2.3.0
path: ^1.8.3
Step 2: Create the Task Model and Database Helper
Use the Task class and DatabaseHelper from Section 4.
Step 3: Build the UI
import 'package:flutter/material.dart';
import 'package:sqflite/sqflite.dart';
void main() {
runApp(const TodoApp());
}
class TodoApp extends StatelessWidget {
const TodoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'To-Do List',
theme: ThemeData(primarySwatch: Colors.blue),
home: const TodoScreen(),
);
}
}
class TodoScreen extends StatefulWidget {
const TodoScreen({super.key});
@override
_TodoScreenState createState() => _TodoScreenState();
}
class _TodoScreenState extends State<TodoScreen> {
final TextEditingController _controller = TextEditingController();
List<Task> _tasks = [];
@override
void initState() {
super.initState();
_loadTasks();
}
Future<void> _loadTasks() async {
final tasks = await DatabaseHelper.instance.getTasks();
setState(() {
_tasks = tasks;
});
}
Future<void> _addTask() async {
if (_controller.text.isNotEmpty) {
final task = Task(
title: _controller.text,
createdAt: DateTime.now(),
);
await DatabaseHelper.instance.insertTask(task);
_controller.clear();
_loadTasks();
}
}
Future<void> _toggleTask(Task task) async {
final updatedTask = Task(
id: task.id,
title: task.title,
isCompleted: !task.isCompleted,
createdAt: task.createdAt,
);
await DatabaseHelper.instance.updateTask(updatedTask);
_loadTasks();
}
Future<void> _deleteTask(int id) async {
await DatabaseHelper.instance.deleteTask(id);
_loadTasks();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('To-Do List')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(labelText: 'New Task'),
),
),
IconButton(
icon: const Icon(Icons.add),
onPressed: _addTask,
),
],
),
),
Expanded(
child: ListView.builder(
itemCount: _tasks.length,
itemBuilder: (context, index) {
final task = _tasks[index];
return ListTile(
title: Text(
task.title,
style: TextStyle(
decoration: task.isCompleted
? TextDecoration.lineThrough
: null,
),
),
leading: Checkbox(
value: task.isCompleted,
onChanged: (value) => _toggleTask(task),
),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _deleteTask(task.id!),
),
);
},
),
),
],
),
);
}
}
Explanation:
UI: A TextField for task input, an IconButton to add tasks, and a ListView to display tasks with checkboxes and delete buttons.
CRUD Operations: Integrates insertTask, getTasks, updateTask, and deleteTask from the DatabaseHelper.
State Management: Uses setState to refresh the task list after CRUD operations.
7.3 Testing and Debugging
Test Data Persistence: Add tasks, close the app, and reopen to verify data retention.
Debugging: Use print statements or Flutter DevTools to inspect database operations.
Error Handling: Wrap CRUD operations in try-catch blocks to handle errors (e.g., database locked).
7.4 Enhancing the App with Advanced Features
Priority Levels: Add a priority column to the tasks table and allow users to set task priority.
Search Functionality: Implement a search bar to filter tasks by title.
Data Backup: Add backup and restore functionality using Section 6.3’s code.
8. Conclusion and Next Steps
This module covered local storage in Flutter using shared_preferences, SQLite with sqflite, and file storage. You learned to:
Save simple key-value data for user settings.
Perform CRUD operations with SQLite for structured data.
Store and retrieve assets like images.
Manage the app data lifecycle with migrations and backups.
Next, explore advanced topics like state management (Provider, Riverpod) and API integration to build more complex apps. Continue practicing by enhancing the to-do list app with features like notifications or cloud sync.
No comments:
Post a Comment
Thanks for your valuable comment...........
Md. Mominul Islam