Todo App with Flutter + Django + MySQL, full guide

Okay, let's build a simple Todo app using Flutter, Django, and MySQL. I'll break it down into steps with explanations for each part.
In Simple Words: What we are building
Imagine you want to keep track of your tasks. This app will let you:
Add tasks (todos): Write down things you need to do.
View tasks: See a list of all your tasks.
Mark tasks as complete: Check off tasks when you finish them.
Delete tasks: Remove tasks you no longer need.
We'll use:
Flutter (Frontend - the part you see and interact with): To create the app's screens on your phone or computer.
Django (Backend - the part that works behind the scenes): To manage your task data, store it, and send it to your Flutter app.
MySQL (Database - where data is stored permanently): To keep your tasks safe even when you close the app.
Let's get started!
Step 1: Set up the Backend with Django and MySQL
This is like building the engine and storage for our app.
(1.1) Install Python and MySQL:
Python: If you don't have Python installed, download it from python.org. Make sure to check the "Add Python to PATH" option during installation.
MySQL: Install MySQL Server. You can download it from mysql.com. During installation, you'll set a root password – remember this! You might also need MySQL Workbench (GUI tool) to manage your database easily.
(1.2) Create a Virtual Environment (Recommended):
This keeps your project's dependencies separate.
Open your terminal or command prompt and navigate to where you want to create your backend project folder. Then run:
python -m venv venv # Creates a virtual environment named 'venv'
source venv/bin/activate # On Linux/macOS
venv\Scripts\activate # On Windows
You'll see (venv) at the start of your terminal prompt, indicating the environment is active.
(1.3) Install Django and Django REST Framework:
Django REST Framework helps us build APIs easily.
pip install django djangorestframework mysqlclient
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution.Bash
IGNORE_WHEN_COPYING_END
django: The main Django framework.
djangorestframework: For building our API.
mysqlclient: Python connector to MySQL.
(1.4) Create a Django Project:
Let's call our project todo_backend.
django-admin startproject todo_backend . # The dot (.) creates the project in the current directory
cd todo_backend
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution.Bash
IGNORE_WHEN_COPYING_END
(1.5) Create a Django App:
Inside your todo_backend directory, create an app called todos. Apps are like modules within your project.
python manage.py startapp todos
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution.Bash
IGNORE_WHEN_COPYING_END
(1.6) Configure Database in Django:
Open todo_backend/settings.py in a text editor. Find the DATABASES section and change it to connect to your MySQL database.
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'todo_db', # Replace 'todo_db' with your database name in MySQL
'USER': 'your_mysql_user', # Replace with your MySQL username (e.g., 'root')
'PASSWORD': 'your_mysql_password', # Replace with your MySQL password
'HOST': 'localhost', # Usually 'localhost' if MySQL is on your computer
'PORT': '3306', # Default MySQL port
}
}
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution.Python
IGNORE_WHEN_COPYING_END
Important:
Create a MySQL Database: Before running Django migrations, you need to create the todo_db database (or whatever you named it in settings.py) in MySQL. You can use MySQL Workbench or the command line:
CREATE DATABASE todo_db; CREATE USER 'your_mysql_user'@'localhost' IDENTIFIED BY 'your_mysql_password'; GRANT ALL PRIVILEGES ON todo_db.* TO 'your_mysql_user'@'localhost'; FLUSH PRIVILEGES;
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution.SQL
IGNORE_WHEN_COPYING_END
Replace 'your_mysql_user' and 'your_mysql_password' with your desired username and password.
(1.7) Define the Todo Model:
Open todos/models.py. This defines how our Todo items will be stored in the database.
from django.db import models
class Todo(models.Model):
title = models.CharField(max_length=200) # Task title, max 200 characters
completed = models.BooleanField(default=False) # Is the task completed?
def __str__(self):
return self.title # How the Todo object is displayed in admin panel etc.
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution.Python
IGNORE_WHEN_COPYING_END
(1.8) Make Migrations and Migrate:
Migrations are how Django updates your database based on your models.
python manage.py makemigrations todos
python manage.py migrate
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution.Bash
IGNORE_WHEN_COPYING_END
This creates the todos_todo table in your todo_db MySQL database.
(1.9) Create Serializers:
Serializers convert Django models to JSON (and back), which is how our Flutter app will communicate with the backend. Create a file todos/serializers.py:
from rest_framework import serializers
from .models import Todo
class TodoSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
fields = ('id', 'title', 'completed') # Fields to include in the API
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution.Python
IGNORE_WHEN_COPYING_END
(1.10) Create API Views:
Views handle requests from the frontend and send responses. Open todos/views.py:
from rest_framework import generics
from .models import Todo
from .serializers import TodoSerializer
class TodoListCreateAPIView(generics.ListCreateAPIView):
queryset = Todo.objects.all() # Get all Todo items
serializer_class = TodoSerializer
class TodoRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = Todo.objects.all() # Get all Todo items
serializer_class = TodoSerializer
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution.Python
IGNORE_WHEN_COPYING_END
TodoListCreateAPIView: For listing all todos (GET) and creating a new todo (POST).
TodoRetrieveUpdateDestroyAPIView: For getting details of a specific todo (GET), updating (PUT/PATCH), and deleting (DELETE).
(1.11) Configure URLs:
Open todos/urls.py (you might need to create this file):
from django.urls import path
from .views import TodoListCreateAPIView, TodoRetrieveUpdateDestroyAPIView
urlpatterns = [
path('todos/', TodoListCreateAPIView.as_view(), name='todo-list-create'),
path('todos/<int:pk>/', TodoRetrieveUpdateDestroyAPIView.as_view(), name='todo-detail'),
]
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution.Python
IGNORE_WHEN_COPYING_END
Include these URLs in your main todo_backend/urls.py:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls), # Django admin panel (optional, but useful)
path('api/', include('todos.urls')), # Include our todos app URLs under /api/
]
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution.Python
IGNORE_WHEN_COPYING_END
(1.12) Add 'rest_framework' and 'todos' to Installed Apps:
In todo_backend/settings.py, add 'rest_framework' and 'todos' to INSTALLED_APPS:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework', # Add REST framework
'todos', # Add our todos app
]
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution.Python
IGNORE_WHEN_COPYING_END
(1.13) Run the Django Development Server:
python manage.py runserver
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution.Bash
IGNORE_WHEN_COPYING_END
You should see "Starting development server at http://127.0.0.1:8000/".
Test your API:
Open a browser or use a tool like Postman.
Go to http://127.0.0.1:8000/api/todos/. You should see an empty JSON array [] (or any todos you might have created through the admin panel).
You can try creating a new todo by sending a POST request to this URL using Postman or curl.
Backend is done! Let's move to Flutter.
Step 2: Build the Frontend with Flutter
This is the part users will see and interact with.
(2.1) Install Flutter and Set up your Environment:
If you haven't already, install Flutter following the instructions on flutter.dev. Make sure Flutter is in your PATH.
(2.2) Create a Flutter Project:
Open your terminal and navigate to where you want to create your Flutter project (outside your Django backend folder).
flutter create todo_flutter_app
cd todo_flutter_app
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution.Bash
IGNORE_WHEN_COPYING_END
(2.3) Add http Package:
We'll use the http package to make API calls to our Django backend. Open pubspec.yaml and add http under dependencies:
dependencies:
flutter:
sdk: flutter
http: ^latest # Add this line (get the latest version from pub.dev)
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution.Yaml
IGNORE_WHEN_COPYING_END
Run flutter pub get in the terminal to download the package.
(2.4) Create a Todo Model in Flutter:
Create a file lib/models/todo.dart:
class Todo {
int? id; // Nullable int for ID (backend assigns it)
String title;
bool completed;
Todo({this.id, required this.title, this.completed = false});
factory Todo.fromJson(Map<String, dynamic> json) {
return Todo(
id: json['id'],
title: json['title'],
completed: json['completed'] ?? false, // Default to false if not in JSON
);
}
Map<String, dynamic> toJson() {
return {
'title': title,
'completed': completed,
};
}
}
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution.Dart
IGNORE_WHEN_COPYING_END
(2.5) Create a Todo Service to Interact with the API:
Create lib/services/todo_service.dart:
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/todo.dart';
class TodoService {
static const String baseUrl = 'http://127.0.0.1:8000/api'; // Django backend URL
static Future<List<Todo>> getTodos() async {
final response = await http.get(Uri.parse('$baseUrl/todos/'));
if (response.statusCode == 200) {
List jsonResponse = json.decode(response.body);
return jsonResponse.map((item) => Todo.fromJson(item)).toList();
} else {
throw Exception('Failed to load todos');
}
}
static Future<Todo> createTodo(Todo todo) async {
final response = await http.post(
Uri.parse('$baseUrl/todos/'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(todo.toJson()),
);
if (response.statusCode == 201) { // 201 Created status
return Todo.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to create todo');
}
}
static Future<Todo> updateTodo(Todo todo) async {
final response = await http.put(
Uri.parse('$baseUrl/todos/${todo.id}/'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(todo.toJson()),
);
if (response.statusCode == 200) {
return Todo.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to update todo');
}
}
static Future<void> deleteTodo(int id) async {
final response = await http.delete(
Uri.parse('$baseUrl/todos/$id/'),
);
if (response.statusCode != 204) { // 204 No Content on successful delete
throw Exception('Failed to delete todo');
}
}
}
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution.Dart
IGNORE_WHEN_COPYING_END
(2.6) Build the UI in main.dart:
Replace the content of lib/main.dart with this:
import 'package:flutter/material.dart';
import './models/todo.dart';
import './services/todo_service.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Todo App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const TodoListScreen(),
);
}
}
class TodoListScreen extends StatefulWidget {
const TodoListScreen({super.key});
@override
_TodoListScreenState createState() => _TodoListScreenState();
}
class _TodoListScreenState extends State<TodoListScreen> {
List<Todo> todos = [];
bool _loading = false;
@override
void initState() {
super.initState();
_loadTodos();
}
Future<void> _loadTodos() async {
setState(() {
_loading = true;
});
try {
todos = await TodoService.getTodos();
} catch (e) {
// Handle error (e.g., show a snackbar)
print('Error loading todos: $e');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to load todos.')),
);
} finally {
setState(() {
_loading = false;
});
}
}
Future<void> _addTodo(String title) async {
setState(() {
_loading = true;
});
try {
final newTodo = Todo(title: title);
await TodoService.createTodo(newTodo);
await _loadTodos(); // Reload todos after adding
} catch (e) {
print('Error adding todo: $e');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to add todo.')),
);
} finally {
setState(() {
_loading = false;
});
}
}
Future<void> _toggleComplete(Todo todo) async {
setState(() {
_loading = true;
});
try {
final updatedTodo = Todo(id: todo.id, title: todo.title, completed: !todo.completed);
await TodoService.updateTodo(updatedTodo);
await _loadTodos();
} catch (e) {
print('Error updating todo: $e');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to update todo.')),
);
} finally {
setState(() {
_loading = false;
});
}
}
Future<void> _deleteTodo(Todo todo) async {
setState(() {
_loading = true;
});
try {
await TodoService.deleteTodo(todo.id!);
await _loadTodos();
} catch (e) {
print('Error deleting todo: $e');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to delete todo.')),
);
} finally {
setState(() {
_loading = false;
});
}
}
void _showAddTodoDialog() {
TextEditingController titleController = TextEditingController();
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Add New Todo'),
content: TextField(
controller: titleController,
decoration: const InputDecoration(hintText: 'Todo title'),
),
actions: <Widget>[
TextButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
),
ElevatedButton(
child: const Text('Add'),
onPressed: () {
if (titleController.text.isNotEmpty) {
_addTodo(titleController.text);
Navigator.of(context).pop();
}
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Todo App'),
),
floatingActionButton: FloatingActionButton(
onPressed: _showAddTodoDialog,
child: const Icon(Icons.add),
),
body: _loading
? const Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
leading: Checkbox(
value: todo.completed,
onChanged: (bool? value) {
if (value != null) {
_toggleComplete(todo);
}
},
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.completed ? TextDecoration.lineThrough : null,
),
),
trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () {
_deleteTodo(todo);
},
),
);
},
),
);
}
}
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution.Dart
IGNORE_WHEN_COPYING_END
(2.7) Run the Flutter App:
Make sure your Django backend is running (python manage.py runserver). Then, in your todo_flutter_app directory, run:
flutter run
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution.Bash
IGNORE_WHEN_COPYING_END
Choose your desired device (emulator, simulator, or physical device).
Step 3: Run and Test
Start Django Backend: If you haven't already, run python manage.py runserver in your todo_backend directory.
Run Flutter Frontend: Run flutter run in your todo_flutter_app directory.
Test the App:
You should see the Todo app running on your device/emulator.
Try adding new tasks by tapping the "+" button.
Check and uncheck tasks to mark them as complete/incomplete.
Delete tasks by tapping the delete icon.
Data should be saved and retrieved from your MySQL database through the Django backend.
Explanation of Key Parts:
Backend (Django):
Models: Define the structure of your data in the database (Todo with title and completed status).
Serializers: Convert data between Python objects and JSON format for API communication.
Views (API Endpoints): Handle HTTP requests (GET, POST, PUT, DELETE) to perform operations on your Todo data.
URLs: Map URLs to your views, making them accessible via HTTP requests.
Frontend (Flutter):
Todo Model (Dart): Represents the Todo data in your Flutter app.
TodoService: Handles communication with the Django API. Makes HTTP requests to get, create, update, and delete todos.
UI (Widgets): Builds the user interface using Flutter widgets:
TodoListScreen: Displays the list of todos, handles loading, adding, updating, and deleting.
ListView.builder: Efficiently displays a scrollable list of items.
Checkbox: For marking todos as completed.
TextField, AlertDialog: For adding new todos.
FloatingActionButton: Button to trigger adding a new todo.
Important Notes:
Error Handling: The code includes basic error handling (showing SnackBars). You should add more robust error handling for production apps.
Styling: This is a very basic UI. You can customize the Flutter UI to make it look much better.
State Management: For a larger app, you might want to use a more advanced state management solution (like Provider, BLoC, Riverpod) instead of setState.
Security: For a real-world app, you'd need to consider security aspects like user authentication and authorization.
CORS: If your Flutter app and Django backend are running on different ports or domains in a real deployment, you might need to configure CORS (Cross-Origin Resource Sharing) in Django to allow requests from your Flutter app's origin. For local development with 127.0.0.1, it usually works without extra CORS configuration.
This guide gives you a foundational Todo app using Flutter, Django, and MySQL. You can now expand upon this, add more features, and improve the UI and functionality. Good luck!
Subscribe to my newsletter
Read articles from Singaraju Saiteja directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
