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

  1. Start Django Backend: If you haven't already, run python manage.py runserver in your todo_backend directory.

  2. Run Flutter Frontend: Run flutter run in your todo_flutter_app directory.

  3. 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!

0
Subscribe to my newsletter

Read articles from Singaraju Saiteja directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Singaraju Saiteja
Singaraju Saiteja