Welcome to the fifth post in our series on mobile app development using Flutter. In this post, we’ll implement local storage functionality in our To-Do list app to achieve data persistence.
Why Do We Need Local Storage?
Data persistence is crucial in mobile apps. Maintaining user data even after the app is closed and reopened provides a better user experience. This is especially important for apps like our To-Do list.
Introduction to SharedPreferences
Flutter provides a plugin called ‘SharedPreferences’ for storing simple key-value pairs of data. This plugin offers an easy way to use iOS’s NSUserDefaults and Android’s SharedPreferences in Flutter.
1. Adding the Package
First, we need to add the shared_preferences
package. Add the following line to your pubspec.yaml
file:
dependencies:
flutter:
sdk: flutter
shared_preferences: ^2.0.8
Then, run this command in your terminal:
flutter pub get
2. Modifying the Code
Now, let’s modify our lib/main.dart
file as follows:
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'To-Do List',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: TodoListScreen(),
);
}
}
class TodoListScreen extends StatefulWidget {
@override
_TodoListScreenState createState() => _TodoListScreenState();
}
class _TodoListScreenState extends State<TodoListScreen> {
List<Todo> todos = [];
@override
void initState() {
super.initState();
_loadTodos();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('My To-Do List'),
),
body: TodoList(todos: todos, onTodoToggle: _toggleTodo),
floatingActionButton: FloatingActionButton(
onPressed: _addTodo,
child: Icon(Icons.add),
),
);
}
void _addTodo() {
showDialog(
context: context,
builder: (BuildContext context) {
String newTodo = "";
return AlertDialog(
title: Text('Add a new todo'),
content: TextField(
onChanged: (value) {
newTodo = value;
},
),
actions: <Widget>[
TextButton(
child: Text('Add'),
onPressed: () {
setState(() {
todos.add(Todo(title: newTodo));
_saveTodos();
});
Navigator.of(context).pop();
},
),
],
);
},
);
}
void _toggleTodo(Todo todo) {
setState(() {
todo.isCompleted = !todo.isCompleted;
_saveTodos();
});
}
void _loadTodos() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? todosString = prefs.getString('todos');
if (todosString != null) {
List<dynamic> todoList = jsonDecode(todosString);
setState(() {
todos = todoList.map((item) => Todo.fromJson(item)).toList();
});
}
}
void _saveTodos() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String todosString = jsonEncode(todos.map((todo) => todo.toJson()).toList());
await prefs.setString('todos', todosString);
}
}
class TodoList extends StatelessWidget {
final List<Todo> todos;
final Function(Todo) onTodoToggle;
TodoList({required this.todos, required this.onTodoToggle});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(
todos[index].title,
style: TextStyle(
decoration: todos[index].isCompleted ? TextDecoration.lineThrough : null,
),
),
trailing: Checkbox(
value: todos[index].isCompleted,
onChanged: (_) => onTodoToggle(todos[index]),
),
);
},
);
}
}
class Todo {
String title;
bool isCompleted;
Todo({required this.title, this.isCompleted = false});
factory Todo.fromJson(Map<String, dynamic> json) {
return Todo(
title: json['title'],
isCompleted: json['isCompleted'],
);
}
Map<String, dynamic> toJson() {
return {
'title': title,
'isCompleted': isCompleted,
};
}
}
3. Code Explanation
_loadTodos()
method: Loads saved To-Do items from SharedPreferences when the app starts. This method is called ininitState()
to load data at app startup._saveTodos()
method: Saves To-Do list to SharedPreferences whenever it changes. This method is called when adding new items or changing completion status.Todo
class: AddedfromJson
andtoJson
methods to enable JSON conversion. This is necessary for storing and retrieving objects.
The Need for JSON Serialization
SharedPreferences can only store basic data types like strings, integers, and booleans. However, our To-Do items are objects. To solve this, we convert To-Do objects to JSON format for storage and back to objects when retrieving. This process is called JSON serialization and deserialization.
The Importance of Asynchronous Programming
Storing and retrieving data from local storage takes time. Processing these operations synchronously on the main thread can degrade app performance and user experience. Therefore, we handle these operations asynchronously. Dart provides ‘async’ and ‘await’ keywords to easily implement asynchronous programming.
Running the App
Start the app by running this command in your terminal:
flutter pub run
Now when you run the app, To-Do items will be saved locally, persisting even when you close and reopen the app.
Wrap-up
In this post, we added local storage functionality to our To-Do list app, implementing data persistence. Through this, we learned about data storage methods in Flutter and the basics of asynchronous programming. By implementing local storage in this way, our To-Do list app now ensures user data persistence, providing a better user experience.
In our next post, we’ll improve the UI of this app and implement additional features such as deleting and editing tasks. If you have any questions about Flutter development, feel free to leave a comment below!
Related Resources
- SharedPreferences package: https://pub.dev/packages/shared_preferences
- JSON serialization in Flutter: https://flutter.dev/docs/development/data-and-backend/json
Related Posts
Flutter Development-3: Implementing a Simple To-Do List App UI – CSAI
Flutter Development-2 Project Creation and Structure Overview – CSAI
Flutter Development-3: Implementing a Simple To-Do List App UI – CSAI
Flutter-4: Adding State Management – CSAI