What You'll Build
A todo list: a text field plus "Add" button, a scrollable list built with ListView.builder, tap-to-toggle-complete, swipe-to-delete, and persistence via shared_preferences so the list survives an app restart.
Prerequisites
- Flutter SDK installed and a new project created (
flutter create todo_app) - Add to
pubspec.yaml:shared_preferences: ^2.2.0 - Helpful but not required: read State Management with setState and Lists with ListView.builder first
Step by Step
1 Define the Todo model
// lib/todo.dart
class Todo {
String title;
bool done;
Todo({required this.title, this.done = false});
Map<String, dynamic> toJson() => {'title': title, 'done': done};
factory Todo.fromJson(Map<String, dynamic> json) =>
Todo(title: json['title'], done: json['done']);
}
toJson/fromJson let the list be serialized to a string for storage, since shared_preferences only stores primitives and string lists.
2 Set up the StatefulWidget and load saved todos
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import 'todo.dart';
void main() => runApp(const TodoApp());
class TodoApp extends StatelessWidget {
const TodoApp({super.key});
@override
Widget build(BuildContext context) =>
MaterialApp(home: const TodoScreen(), debugShowCheckedModeBanner: false);
}
class TodoScreen extends StatefulWidget {
const TodoScreen({super.key});
@override
State<TodoScreen> createState() => _TodoScreenState();
}
class _TodoScreenState extends State<TodoScreen> {
List<Todo> _todos = [];
final _controller = TextEditingController();
@override
void initState() {
super.initState();
_loadTodos();
}
Future<void> _loadTodos() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getStringList('todos') ?? [];
setState(() {
_todos = saved.map((s) => Todo.fromJson(jsonDecode(s))).toList();
});
}
Future<void> _saveTodos() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setStringList(
'todos',
_todos.map((t) => jsonEncode(t.toJson())).toList(),
);
}
// step 3 methods go here
// step 4 build() goes here
}
3 Add, toggle, and delete methods
void _addTodo() {
final text = _controller.text.trim();
if (text.isEmpty) return;
setState(() {
_todos.insert(0, Todo(title: text));
_controller.clear();
});
_saveTodos();
}
void _toggleTodo(int index) {
setState(() => _todos[index].done = !_todos[index].done);
_saveTodos();
}
void _deleteTodo(int index) {
setState(() => _todos.removeAt(index));
_saveTodos();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Every mutating method calls setState to trigger a rebuild, then _saveTodos() to persist — keeping the in-memory list and disk copy always in sync.
4 Build the UI
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('My Todos')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(hintText: 'Add a task...'),
onSubmitted: (_) => _addTodo(),
),
),
IconButton(icon: const Icon(Icons.add), onPressed: _addTodo),
],
),
),
Expanded(
child: _todos.isEmpty
? const Center(child: Text('No tasks yet — add one above'))
: ListView.builder(
itemCount: _todos.length,
itemBuilder: (context, index) {
final todo = _todos[index];
return Dismissible(
key: ValueKey(todo.title + index.toString()),
onDismissed: (_) => _deleteTodo(index),
background: Container(color: Colors.red),
child: CheckboxListTile(
title: Text(
todo.title,
style: todo.done
? const TextStyle(decoration: TextDecoration.lineThrough, color: Colors.grey)
: null,
),
value: todo.done,
onChanged: (_) => _toggleTodo(index),
),
);
},
),
),
],
),
);
}
}
Dismissible gives swipe-to-delete for free — wrapping each row and calling the delete method in onDismissed.
5 Run it
Run flutter run, add a few tasks, tap a checkbox to complete one, swipe another to delete it, then restart the app — your remaining todos should still be there with their completed state intact.
Final Working Code
Two files — lib/todo.dart and lib/main.dart (steps 2–4 combined in order) — form the complete app exactly as shown; nothing was trimmed for the walkthrough.