Welcome back! In Part 1, we built a working notes UI with in-memory state. The app looks great — but every time you close it, your notes disappear.
In Part 2, we fix that. By the end of this article, your notes will survive app restarts using local storage. We’ll also add the ability to delete notes and display timestamps — and we’ll keep the code clean using a simple storage service.
What you’ll build in Part 2:
- Persistent notes using the
shared_preferencespackage - A
NotesStorageservice class to keep UI code clean - Delete notes with a swipe or button
- Timestamps displayed on each note
1. Choosing a Storage Approach
Flutter has several local storage options. Here’s a quick comparison so you understand the trade-offs:
| Package | Best for | Complexity |
|---|---|---|
| shared_preferences | Small key-value data, simple lists | ⭐ Beginner-friendly |
| Hive | Structured objects, faster reads | ⭐⭐ Intermediate |
| sqflite | Relational data, complex queries | ⭐⭐⭐ Advanced |
For our simple notes app, shared_preferences is the perfect fit. We’ll store notes as a list of JSON strings. If your app ever grows to hundreds of notes with search and filtering, that’s when you’d graduate to Hive or sqflite.
2. Add the shared_preferences Package
Open pubspec.yaml and add the dependency under dependencies:
dependencies:
flutter:
sdk: flutter
shared_preferences: ^2.3.0
Then run this in your terminal to install it:
flutter pub get
You may need to stop and restart the app after adding a new package.
3. Update the Note Model for JSON
To store notes in shared_preferences, we need to convert them to and from JSON strings. Update lib/models/note.dart:
import 'dart:convert';
class Note {
final String id;
String title;
String content;
final DateTime createdAt;
Note({
required this.id,
required this.title,
required this.content,
required this.createdAt,
});
// Convert a Note into a Map (for JSON encoding)
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'content': content,
'createdAt': createdAt.toIso8601String(),
};
}
// Create a Note from a Map (for JSON decoding)
factory Note.fromJson(Map<String, dynamic> json) {
return Note(
id: json['id'] as String,
title: json['title'] as String,
content: json['content'] as String,
createdAt: DateTime.parse(json['createdAt'] as String),
);
}
// Helper to encode a Note directly to a JSON string
String toJsonString() => jsonEncode(toJson());
// Helper to decode a Note directly from a JSON string
static Note fromJsonString(String jsonString) {
return Note.fromJson(jsonDecode(jsonString) as Map<String, dynamic>);
}
}
We added two conversion methods:
toJsonString()— turns a Note into a string we can store.fromJsonString()— turns a stored string back into a Note.
4. Create the Storage Service
Instead of calling shared_preferences directly inside our UI widgets, we’ll create a dedicated service class. This keeps our code organized and easy to change later (e.g., if you ever swap shared_preferences for Hive, you only change one file).
Create lib/services/notes_storage.dart:
import 'package:shared_preferences/shared_preferences.dart';
import '../models/note.dart';
class NotesStorage {
static const String _storageKey = 'notes_list';
// Load all notes from storage
Future<List<Note>> loadNotes() async {
final prefs = await SharedPreferences.getInstance();
final List<String> jsonStrings = prefs.getStringList(_storageKey) ?? [];
return jsonStrings.map((s) => Note.fromJsonString(s)).toList();
}
// Save the full notes list to storage
Future<void> saveNotes(List<Note> notes) async {
final prefs = await SharedPreferences.getInstance();
final List<String> jsonStrings = notes.map((n) => n.toJsonString()).toList();
await prefs.setStringList(_storageKey, jsonStrings);
}
}
This is intentionally simple. Two methods — loadNotes() and saveNotes() — are all we need. The UI doesn’t need to know anything about JSON or shared_preferences keys.
5. Update the Notes List Screen
Now we connect the storage service to our UI. Replace the contents of lib/screens/notes_list_page.dart:
import 'package:flutter/material.dart';
import '../models/note.dart';
import '../services/notes_storage.dart';
import 'note_editor_page.dart';
class NotesListPage extends StatefulWidget {
const NotesListPage({super.key});
@override
State<NotesListPage> createState() => _NotesListPageState();
}
class _NotesListPageState extends State<NotesListPage> {
final NotesStorage _storage = NotesStorage();
List<Note> _notes = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadNotes();
}
Future<void> _loadNotes() async {
final notes = await _storage.loadNotes();
setState(() {
_notes = notes;
_isLoading = false;
});
}
Future<void> _saveNotes() async {
await _storage.saveNotes(_notes);
}
void _openEditor({Note? note, int? index}) async {
final result = await Navigator.push<Note>(
context,
MaterialPageRoute(
builder: (context) => NoteEditorPage(existingNote: note),
),
);
if (result != null) {
setState(() {
if (index != null) {
_notes[index] = result;
} else {
_notes.insert(0, result); // Add new notes to the top
}
});
await _saveNotes(); // Persist after every change
}
}
void _deleteNote(int index) async {
setState(() {
_notes.removeAt(index);
});
await _saveNotes();
}
String _formatDate(DateTime date) {
final now = DateTime.now();
final diff = now.difference(date);
if (diff.inDays == 0) return 'Today';
if (diff.inDays == 1) return 'Yesterday';
if (diff.inDays < 7) return '${diff.inDays} days ago';
return '${date.day}/${date.month}/${date.year}';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My Notes'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _notes.isEmpty
? const Center(
child: Text(
'No notes yet.nTap + to create one!',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, color: Colors.grey),
),
)
: ListView.builder(
itemCount: _notes.length,
itemBuilder: (context, index) {
final note = _notes[index];
return Dismissible(
key: Key(note.id),
direction: DismissDirection.endToStart,
background: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.symmetric(horizontal: 20),
color: Colors.red,
child: const Icon(Icons.delete, color: Colors.white),
),
onDismissed: (_) => _deleteNote(index),
child: ListTile(
title: Text(
note.title.isEmpty ? 'Untitled' : note.title,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (note.content.isNotEmpty)
Text(
note.content,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
_formatDate(note.createdAt),
style: const TextStyle(
fontSize: 11,
color: Colors.grey,
),
),
],
),
onTap: () => _openEditor(note: note, index: index),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _openEditor(),
tooltip: 'New Note',
child: const Icon(Icons.add),
),
);
}
}
What changed from Part 1:
- Loading on startup —
initState()now calls_loadNotes(), which fetches saved notes from storage before the first frame renders. - Loading indicator — while notes are being loaded, we show a
CircularProgressIndicatorinstead of an empty screen. - Auto-save — every time a note is added, edited, or deleted,
_saveNotes()is called immediately after. - Swipe to delete — wrapping each
ListTilein aDismissiblewidget gives us swipe-to-delete for free. Swipe left on any note to reveal the red delete background. - Timestamps — each note now shows a human-friendly date label (Today, Yesterday, X days ago, or a full date).
6. Final File Structure
Your lib/ folder should now look like this:
lib/
├── main.dart
├── models/
│ └── note.dart
├── screens/
│ ├── notes_list_page.dart
│ └── note_editor_page.dart
└── services/
└── notes_storage.dart
This “structure by feature type” layout is a solid foundation. As your app grows, you can evolve it into a feature-first structure — but for a beginner project this size, it’s clean and easy to navigate.
7. Run and Test Persistence
Run the app:
flutter run
Try this to verify persistence is working:
- Create two or three notes.
- Completely close the app (don’t just background it — fully quit it).
- Reopen the app.
- Your notes should still be there. ✅
Also test swipe-to-delete: swipe any note from right to left to delete it, then close and reopen the app to confirm the deletion was also persisted.
Best Practices to Carry Forward
Before we wrap up, here are a few habits worth building from the start:
Keep build methods lean
Never do heavy work (I/O, network calls, expensive calculations) inside build(). The build method can be called many times per second. Do that work in initState(), dedicated service classes, or triggered by user actions.
Always use ListView.builder for lists
Unlike putting widgets in a Column, ListView.builder only creates the widgets that are currently visible on screen. This keeps scrolling smooth even with hundreds of items.
Dispose your controllers
Any time you create a TextEditingController, AnimationController, or similar object, always override dispose() and clean them up. Forgetting this causes memory leaks.
Separate concerns
Our NotesStorage service is a small example of this principle. UI widgets shouldn’t know how data is stored — they should just call a service. This makes it trivial to swap out shared_preferences for Hive or a remote API later without touching your UI code.
What to Learn Next
You’ve built a fully functional, persistent notes app from scratch. Here’s where to go next depending on what interests you:
- State management — For larger apps, look into Provider or Riverpod to manage state more cleanly across many screens.
- Better local storage — Try Hive or sqflite when you need structured data, relationships, or faster queries.
- Search and filtering — Add a search bar to the notes list by filtering
_noteswith a search query in state. - Cloud sync — Explore Firebase Firestore to sync notes across devices.
- Official Flutter docs — The Flutter learning pathway is excellent and free.
Series Recap
Over these two articles, you went from zero to a fully working notes app:
- Part 1 — Flutter setup, widgets, layouts, multi-screen navigation, in-memory state.
- Part 2 — Local persistence with shared_preferences, a storage service, swipe-to-delete, and timestamps.
That covers the full beginner foundation: project structure, UI building blocks, navigation, state management, and data persistence. Everything you build from here will use these same concepts — just at a larger scale. Happy coding!

Pingback: Flutter Notes App Part 1: Project Setup & Core UI (2026)
Pingback: Flutter Tutorial for Beginners Step by Step (2026)
Pingback: Handling User Input in Flutter: TextField, Forms and Validation for Beginners (2026)
Pingback: Flutter Local Storage: SharedPreferences vs Hive vs sqflite — Which to Use?
Pingback: Flutter Layout Made Easy: Row, Column, Flex and Expanded for Beginners (2026) - Flutter for beginners