Task management app Flutter + fastapi + Mysql

Okay, let's build a task management app using Flutter for the frontend, FastAPI for the backend, and MySQL for the database. This is a fantastic project for a fresher as it covers all three core layers of a typical web/mobile application stack: database, backend logic, and frontend UI.
We'll break this down step-by-step, explaining the essential concepts for each technology as we go.
Project Goal: A simple task management app where users can view, add, mark as complete, and delete tasks.
Architecture:
+-------------+ +--------------+ +---------+
| Flutter | <----> | FastAPI | <----> | MySQL |
| (Frontend) | HTTP | (Backend) | SQL | (Database)|
+-------------+ +--------------+ +---------+
UI API Endpoints Data Storage
User Input Business Logic
Database Interaction
Why this stack for a Fresher?
Flutter:
Cross-Platform: Learn one language (Dart) and build for iOS, Android, Web, and Desktop. Hugely efficient for beginners.
Widget-Based: UI is built like Lego blocks (widgets), making it intuitive.
Excellent Documentation: Flutter has one of the best documentations available.
Hot Reload: See changes instantly, speeding up development.
Importance: High demand for mobile and cross-platform developers.
FastAPI:
Python: Uses Python, one of the easiest and most popular languages.
Speed: Very fast performance (hence the name).
Automatic Docs: Generates interactive API documentation (Swagger UI / OpenAPI) automatically, which is incredibly useful for testing and understanding your API.
Modern: Built on modern Python features (async/await, type hints).
Importance: Python backends are popular, and FastAPI is quickly becoming a go-to for building efficient APIs.
MySQL:
Widely Used: One of the most popular relational databases in the world. Learning it is a fundamental skill.
Structured Data: Teaches you about organizing data in tables, relationships, and using SQL.
Reliable: Mature and robust for production use.
Importance: Almost every application needs to store data somewhere persistently. Understanding databases is non-negotiable.
Prerequisites:
Python: Install Python 3.7+ (python.org).
MySQL: Install MySQL Server and a client tool (like MySQL Workbench or DBeaver) (dev.mysql.com/downloads).
Flutter SDK: Install Flutter (flutter.dev/docs/get-started/install).
IDE: VS Code (recommended) or Android Studio/IntelliJ with Python and Flutter plugins.
Part 1: The Database (MySQL)
Essential Elements:
Database: A container for tables.
Table: A structured collection of data related to a single topic (like tasks).
Columns: The attributes of each item in the table (e.g., id, title, description).
Rows: Individual records in the table (e.g., one specific task).
SQL: The standard language for interacting with relational databases (Structured Query Language). Commands like CREATE DATABASE, CREATE TABLE, INSERT, SELECT, UPDATE, DELETE.
Primary Key: A column (or set of columns) that uniquely identifies each row. Often auto-incrementing integers (id).
Data Types: Defining what kind of data a column holds (e.g., VARCHAR for text, INT for integers, BOOLEAN for true/false).
Importance for a Fresher: This is where your application's persistent data lives. Learning how to design a simple schema and interact with it using SQL is fundamental to almost all backend development. You learn how data is structured and stored.
Steps:
Connect to MySQL: Use your client tool (MySQL Workbench, DBeaver, or the command line).
Create Database: Execute the following SQL command:
CREATE DATABASE task_app_db;
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. SQL
IGNORE_WHEN_COPYING_END
Use the Database:
USE task_app_db;
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. SQL
IGNORE_WHEN_COPYING_END
Create Tasks Table: Create a table to store tasks.
CREATE TABLE tasks ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, description TEXT, is_completed BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. SQL
IGNORE_WHEN_COPYING_END
id INT AUTO_INCREMENT PRIMARY KEY: Unique identifier, automatically increases with each new task.
title VARCHAR(255) NOT NULL: Task title, required (cannot be NULL). Limited to 255 characters.
description TEXT: Optional longer description.
is_completed BOOLEAN DEFAULT FALSE: Status of the task, defaults to not complete.
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP: Automatically records when the task was created.
(Optional) Insert some sample data:
INSERT INTO tasks (title, description) VALUES ('Learn FastAPI', 'Go through the documentation and tutorials.'), ('Build Flutter UI', 'Create screens for task list and add/edit.'), ('Connect Frontend & Backend', 'Make API calls from Flutter.');
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. SQL
IGNORE_WHEN_COPYING_END
(We'll manage data via FastAPI later, this is just for testing)
You now have a database and a table ready to store your task data.
Part 2: The Backend (FastAPI)
Essential Elements:
API: Application Programming Interface. A set of rules and definitions for how different software components interact. Our FastAPI app will be the API that Flutter talks to.
HTTP Methods: GET (retrieve data), POST (create data), PUT (update data), DELETE (delete data). These map directly to our CRUD operations.
Routes (Endpoints): Specific URLs that the API responds to (e.g., /tasks for listing tasks, /tasks/{task_id} for a specific task).
Request/Response: The data sent to the API (request body, parameters) and the data sent back from the API (response body, status code).
Pydantic: A library used by FastAPI for data validation and serialization. You define data structures (schemas) using BaseModel. FastAPI automatically validates incoming request data and formats outgoing response data based on these. Crucial for clean, safe APIs.
SQLAlchemy: An ORM (Object Relational Mapper) for Python. It allows you to interact with databases using Python objects and methods instead of writing raw SQL strings. Makes database interaction much cleaner in Python.
Dependency Injection: FastAPI's way of providing components (like a database session) to your route functions automatically. Makes code more modular and testable.
Importance for a Fresher: The backend is the brain. It handles the logic, interacts with the database, and provides a structured way for the frontend to get and manipulate data. Learning backend principles is vital for building any dynamic application. You learn data handling, business logic, and API design.
Steps:
Set up Project:
Create a folder for your backend (e.g., fastapi_backend).
Open a terminal in that folder.
Create a virtual environment (recommended):
python -m venv venv # On Windows: venv\Scripts\activate # On macOS/Linux: source venv/bin/activate
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. Bash
IGNORE_WHEN_COPYING_END
Install necessary libraries:
pip install fastapi uvicorn[standard] sqlalchemy mysql-connector-python pydantic # Or (using newer driver): pip install fastapi uvicorn[standard] sqlalchemy aiomysql # if you want async, slightly more complex setup
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. Bash
IGNORE_WHEN_COPYING_END
(We'll use mysql-connector-python for synchronous connections for simplicity, though async (aiomysql) is common with FastAPI)
Project Structure: Let's create a simple structure:
fastapi_backend/ ├── main.py # FastAPI app instance, routes ├── database.py # Database connection setup ├── models.py # SQLAlchemy models (Python representation of DB tables) ├── schemas.py # Pydantic models (data validation/serialization) ├── crud.py # Functions for Create, Read, Update, Delete operations └── requirements.txt # List of installed packages (pip freeze > requirements.txt)
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution.
IGNORE_WHEN_COPYING_END
database.py: Set up the database connection.
from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker # Replace with your actual database credentials DATABASE_URL = "mysql+mysqlconnector://user:password@host/task_app_db" # Example: "mysql+mysqlconnector://root:mypassword@localhost/task_app_db" engine = create_engine(DATABASE_URL) # Each instance of the SessionLocal class will be a database session # The SessionLocal class itself is not a database session yet. SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) # We will inherit from this to create each of the database models. Base = declarative_base() # Dependency to get DB session def get_db(): db = SessionLocal() try: yield db finally: db.close()
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. Python
IGNORE_WHEN_COPYING_END
create_engine: Sets up the connection to the database.
SessionLocal: A factory to create database sessions.
declarative_base: The base class for your SQLAlchemy models.
get_db: A dependency function. FastAPI will call this when a route needs a db session, manage the session lifecycle (open, close), and pass it to the route function.
models.py: Define SQLAlchemy models corresponding to your tasks table.
from sqlalchemy import Column, Integer, String, Boolean, DateTime from .database import Base # Use relative import if in same package from sqlalchemy.sql import func class Task(Base): __tablename__ = "tasks" # Link to your MySQL table name id = Column(Integer, primary_key=True, index=True) title = Column(String(255), index=True) description = Column(String) # TEXT in MySQL maps generally to String in SQLAlchemy is_completed = Column(Boolean, default=False) created_at = Column(DateTime, server_default=func.now()) # Use server_default with func.now() for MySQL timestamp
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. Python
IGNORE_WHEN_COPYING_END
Defines a Python class Task that maps to the tasks table.
Each Column maps to a table column, specifying its type and properties (like primary_key, index).
schemas.py: Define Pydantic models for data validation/serialization.
from pydantic import BaseModel, Field from datetime import datetime class TaskBase(BaseModel): title: str = Field(..., max_length=255) # ... means required, max_length for validation description: str | None = None # Optional string is_completed: bool = False class TaskCreate(TaskBase): # Add fields specific to creation if any (none needed here) pass class TaskUpdate(TaskBase): # Allow updating any base field pass class Task(TaskBase): id: int created_at: datetime class Config: orm_mode = True # Allows SQLAlchemy models to be converted to Pydantic models from_attributes = True # Newer SQLAlchemy versions use from_attributes
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. Python
IGNORE_WHEN_COPYING_END
TaskBase: Common fields for tasks.
TaskCreate, TaskUpdate: Models for incoming data (request body). Using TaskBase as a base promotes reusability.
Task: Model for outgoing data (response body). Includes id and created_at which are database-generated. orm_mode = True is essential for converting the SQLAlchemy Task object to this Pydantic schema automatically.
crud.py: Write functions for interacting with the database using SQLAlchemy.
from sqlalchemy.orm import Session from . import models, schemas def get_task(db: Session, task_id: int): return db.query(models.Task).filter(models.Task.id == task_id).first() def get_tasks(db: Session, skip: int = 0, limit: int = 100): return db.query(models.Task).offset(skip).limit(limit).all() def create_task(db: Session, task: schemas.TaskCreate): db_task = models.Task(**task.model_dump()) # Convert Pydantic model to SQLAlchemy model db.add(db_task) db.commit() db.refresh(db_task) # Refresh to get the generated ID and timestamp return db_task def update_task(db: Session, task_id: int, task: schemas.TaskUpdate): db_task = db.query(models.Task).filter(models.Task.id == task_id).first() if db_task: # Update attributes from schema for var, value in task.model_dump(exclude_unset=True).items(): # exclude_unset=True updates only provided fields setattr(db_task, var, value) db.add(db_task) db.commit() db.refresh(db_task) return db_task def delete_task(db: Session, task_id: int): db_task = db.query(models.Task).filter(models.Task.id == task_id).first() if db_task: db.delete(db_task) db.commit() return db_task # Or True for success indication return None
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. Python
IGNORE_WHEN_COPYING_END
These functions encapsulate the database logic.
They accept a db: Session dependency and Pydantic models (schemas.TaskCreate, schemas.TaskUpdate).
They use db.query, filter, first, all, add, commit, refresh, delete from SQLAlchemy ORM.
main.py: Create FastAPI app and define routes.
from fastapi import FastAPI, Depends, HTTPException from sqlalchemy.orm import Session from fastapi.middleware.cors import CORSMiddleware # Needed for local development from . import crud, models, schemas from .database import SessionLocal, engine, get_db # Use relative imports # Create database tables if they don't exist (development convenience) # In production, use migration tools like Alembic models.Base.metadata.create_all(bind=engine) app = FastAPI() # Allow CORS for local development (Flutter app runs on a different port) app.add_middleware( CORSMiddleware, allow_origins=["*"], # Allows all origins - restrict this in production allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.get("/") def read_root(): return {"message": "Welcome to the Task App API"} # --- Task Endpoints --- @app.post("/tasks/", response_model=schemas.Task) def create_task(task: schemas.TaskCreate, db: Session = Depends(get_db)): return crud.create_task(db=db, task=task) @app.get("/tasks/", response_model=list[schemas.Task]) def read_tasks(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): tasks = crud.get_tasks(db, skip=skip, limit=limit) return tasks @app.get("/tasks/{task_id}", response_model=schemas.Task) def read_task(task_id: int, db: Session = Depends(get_db)): db_task = crud.get_task(db, task_id=task_id) if db_task is None: raise HTTPException(status_code=404, detail="Task not found") return db_task @app.put("/tasks/{task_id}", response_model=schemas.Task) def update_task(task_id: int, task: schemas.TaskUpdate, db: Session = Depends(get_db)): db_task = crud.update_task(db, task_id=task_id, task=task) if db_task is None: raise HTTPException(status_code=404, detail="Task not found") return db_task @app.delete("/tasks/{task_id}", response_model=schemas.Task) # Or response_model=dict for confirmation def delete_task(task_id: int, db: Session = Depends(get_db)): db_task = crud.delete_task(db, task_id=task_id) if db_task is None: raise HTTPException(status_code=404, detail="Task not found") # return {"message": "Task deleted successfully", "task_id": task_id} return db_task # Return the deleted task details
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. Python
IGNORE_WHEN_COPYING_END
FastAPI(): Creates the application instance.
models.Base.metadata.create_all(bind=engine): Tells SQLAlchemy to create the tables defined in models.py based on the database connection. Only use this in development or for initial setup, not for handling schema changes later.
CORSMiddleware: Essential during local development because your Flutter app (e.g., on localhost:####) will be requesting resources from your FastAPI app (e.g., on localhost:8000). CORS prevents browsers from blocking these requests. allow_origins=["*"] is permissive for development; restrict this in production.
@app.get(...), @app.post(...), etc.: Decorators that define the route path and HTTP method.
response_model: Tells FastAPI to validate and format the outgoing response using the specified Pydantic schema.
task: schemas.TaskCreate: FastAPI expects a JSON request body that matches the TaskCreate schema, automatically validates it, and passes it as a Python object task.
db: Session = Depends(get_db): This is dependency injection. FastAPI calls get_db(), gets the DB session from the yield, and passes it to the route function. It also handles the finally block (closing the session).
HTTPException: Used to return standard HTTP error responses (like 404 Not Found).
Run the Backend:
In your terminal, navigate to the fastapi_backend folder.
Activate your virtual environment (source venv/bin/activate).
Run the command:
uvicorn main:app --reload
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. Bash
IGNORE_WHEN_COPYING_END
main:app: Tells uvicorn to run the app object inside main.py.
--reload: Restarts the server whenever code changes, convenient for development.
Test the API: Open your browser or a tool like Postman/Insomnia and go to http://localhost:8000/docs. You'll see the automatically generated Swagger UI where you can interact with your API endpoints!
You now have a working backend that connects to your MySQL database and provides API endpoints for managing tasks.
Part 3: The Frontend (Flutter)
Essential Elements:
Widgets: The fundamental building blocks of the Flutter UI. Everything is a widget (buttons, text, layout containers, screens themselves).
Widget Tree: Widgets are organized in a tree structure, defining the UI layout.
Stateless vs. Stateful Widgets:
StatelessWidget: UI that doesn't change after creation.
StatefulWidget: UI that can change dynamically based on user interaction or data updates. This is crucial for displaying data that loads or changes.
State Management: How to manage data that affects the UI and trigger UI updates. Simple setState is a good start for small apps within a StatefulWidget. For larger apps, you'd look into Provider, Riverpod, BLoC, etc.
Navigation: Moving between different screens (Navigator).
Asynchronous Operations (Future, async, await): Network requests (like calling your API) take time. Flutter uses Future to represent a value that will be available later. async and await make working with Futures look like synchronous code. Crucial for non-blocking UI.
FutureBuilder: A widget that helps build UI based on the state of a Future (loading, complete with data, complete with error). Very useful for displaying data fetched from an API.
HTTP Package: Used to make network requests (GET, POST, etc.) to your backend API.
JSON Serialization/Deserialization: Converting Python/API data (JSON) into Dart objects and vice-versa (dart:convert).
Importance for a Fresher: This is what the user sees and interacts with. Learning UI development principles, handling user input, displaying data, and making network calls is fundamental for building user-facing applications. You learn how to create interactive interfaces.
Steps:
Set up Project:
Open your terminal.
Navigate to the desired directory (outside your backend folder).
Create a new Flutter project:
flutter create task_app_frontend
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. Bash
IGNORE_WHEN_COPYING_END
Open the project in your IDE (VS Code recommended).
Add HTTP Dependency:
Open pubspec.yaml.
Add the http package under dependencies:
dependencies: flutter: sdk: flutter http: ^1.1.0 # Use the latest version cupertino_icons: ^1.0.2 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. Yaml
IGNORE_WHEN_COPYING_END
Run flutter pub get in the terminal or let your IDE do it.
Define Task Model: Create a Dart class to represent a Task, mirroring your backend schema.
Create a folder lib/models.
Create lib/models/task.dart:
class Task { final int id; final String title; final String? description; // Nullable final bool isCompleted; final DateTime createdAt; Task({ required this.id, required this.title, this.description, // Optional required this.isCompleted, required this.createdAt, }); // Factory constructor to create a Task from JSON (Map) factory Task.fromJson(Map<String, dynamic> json) { return Task( id: json['id'], title: json['title'], description: json['description'], isCompleted: json['is_completed'] ?? false, // Handle potential null/missing with default createdAt: DateTime.parse(json['created_at']), // Parse ISO 8601 string ); } // Method to convert a Task object to JSON (Map) for sending to backend Map<String, dynamic> toJson() { return { 'title': title, 'description': description, 'is_completed': isCompleted, // Don't include id or createdAt for create/update requests typically }; } }
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. Dart
IGNORE_WHEN_COPYING_END
factory Task.fromJson: This is how you'll create Task objects from the JSON data received from your FastAPI backend. Pay attention to key names matching your backend (is_completed, created_at).
toJson: This is how you'll convert a Dart Task object (or parts of it) into a JSON map to send in POST or PUT requests.
Create API Service: A class to handle HTTP requests to your backend.
Create a folder lib/services.
Create lib/services/api_service.dart:
import 'dart:convert'; // For jsonEncode, jsonDecode import 'package:http/http.dart' as http; // Alias http import '../models/task.dart'; // Import your Task model class ApiService { // Use your backend IP address if running on a physical device // or 'localhost' / '10.0.2.2' (Android emulator alias) final String baseUrl = 'http://localhost:8000'; // Change if needed // --- GET all tasks --- Future<List<Task>> getTasks() async { final response = await http.get(Uri.parse('$baseUrl/tasks/')); if (response.statusCode == 200) { // Decode the JSON array List jsonResponse = jsonDecode(response.body); // Map each item in the list to a Task object return jsonResponse.map((taskJson) => Task.fromJson(taskJson)).toList(); } else { // If the server did not return a 200 OK response, // throw an exception. throw Exception('Failed to load tasks: ${response.statusCode}'); } } // --- POST create task --- Future<Task> createTask(Task task) async { final response = await http.post( Uri.parse('$baseUrl/tasks/'), headers: <String, String>{ 'Content-Type': 'application/json; charset=UTF-8', }, body: jsonEncode(task.toJson()), // Encode Dart object to JSON string ); if (response.statusCode == 200) { // FastAPI POST returns 200 on success by default return Task.fromJson(jsonDecode(response.body)); } else { throw Exception('Failed to create task: ${response.statusCode}'); } } // --- PUT update task --- Future<Task> updateTask(int id, Task task) async { final response = await http.put( Uri.parse('$baseUrl/tasks/$id'), headers: <String, String>{ 'Content-Type': 'application/json; charset=UTF-8', }, body: jsonEncode(task.toJson()), ); if (response.statusCode == 200) { return Task.fromJson(jsonDecode(response.body)); } else { throw Exception('Failed to update task: ${response.statusCode}'); } } // --- DELETE task --- Future<void> deleteTask(int id) async { final response = await http.delete(Uri.parse('$baseUrl/tasks/$id')); if (response.statusCode != 200) { throw Exception('Failed to delete task: ${response.statusCode}'); } // For delete, often no body is returned or it's just a confirmation, // so we return Future<void> and check status code. } }
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. Dart
IGNORE_WHEN_COPYING_END
http.get, http.post, http.put, http.delete: Functions from the http package to make requests.
Uri.parse: Converts the URL string into a Uri object required by http.
jsonDecode, jsonEncode: From dart:convert to handle JSON strings.
await: Pauses execution until the Future completes (the HTTP response is received). Requires the function to be async.
Status Code Check: Important to verify the request was successful (200 OK).
Create Task List Screen: Display the tasks fetched from the API.
Modify lib/main.dart or create a new file (e.g., lib/screens/task_list_screen.dart). Let's modify main.dart for simplicity.
Replace the existing MyApp and MyHomePage with:
import 'package:flutter/material.dart'; import 'models/task.dart'; // Import your Task model import 'services/api_service.dart'; // Import your API service void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Task App', theme: ThemeData( primarySwatch: Colors.blue, ), home: TaskListScreen(), // Start with the TaskListScreen ); } } class TaskListScreen extends StatefulWidget { const TaskListScreen({super.key}); @override _TaskListScreenState createState() => _TaskListScreenState(); } class _TaskListScreenState extends State<TaskListScreen> { late Future<List<Task>> _tasksFuture; // Use late to initialize later final ApiService _apiService = ApiService(); // Create service instance @override void initState() { super.initState(); _tasksFuture = _apiService.getTasks(); // Fetch tasks when the widget is created } // Method to refresh the list void _refreshTaskList() { setState(() { _tasksFuture = _apiService.getTasks(); // Re-fetch tasks }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Tasks'), actions: [ IconButton( icon: const Icon(Icons.refresh), onPressed: _refreshTaskList, // Add refresh button ), ], ), body: FutureBuilder<List<Task>>( // Use FutureBuilder to handle the async fetch future: _tasksFuture, // The future we are waiting for builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { // While data is loading return const Center(child: CircularProgressIndicator()); } else if (snapshot.hasError) { // If an error occurred return Center(child: Text('Error: ${snapshot.error}')); } else if (!snapshot.hasData || snapshot.data!.isEmpty) { // If no data is returned return const Center(child: Text('No tasks found.')); } else { // If data is successfully loaded List<Task> tasks = snapshot.data!; // Get the list of tasks return ListView.builder( // Build a scrollable list itemCount: tasks.length, itemBuilder: (context, index) { final task = tasks[index]; return ListTile( // Display each task as a list tile title: Text( task.title, style: TextStyle( decoration: task.isCompleted ? TextDecoration.lineThrough : null, ), ), subtitle: Text(task.description ?? 'No description'), // Handle null description leading: Checkbox( // Checkbox to mark as complete value: task.isCompleted, onChanged: (bool? value) { // Handle checkbox change (update task status) if (value != null) { // Create an updated task object (copy with new status) final updatedTask = Task( id: task.id, title: task.title, description: task.description, isCompleted: value, // New status createdAt: task.createdAt, ); _apiService.updateTask(task.id, updatedTask).then((_) { _refreshTaskList(); // Refresh list after update }).catchError((error) { // Handle update error (e.g., show a snackbar) print('Error updating task: $error'); }); } }, ), trailing: IconButton( // Button to delete task icon: const Icon(Icons.delete, color: Colors.red), onPressed: () { // Handle delete button press _apiService.deleteTask(task.id).then((_) { _refreshTaskList(); // Refresh list after delete }).catchError((error) { // Handle delete error print('Error deleting task: $error'); }); }, ), onTap: () { // TODO: Navigate to Edit Task Screen later print('Task ${task.title} tapped'); }, ); }, ); } }, ), floatingActionButton: FloatingActionButton( onPressed: () { // TODO: Navigate to Add New Task Screen later print('Add task button pressed'); // Example: Navigator.push(context, MaterialPageRoute(builder: (context) => AddTaskScreen())); }, tooltip: 'Add Task', child: const Icon(Icons.add), ), ); } }
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. Dart
IGNORE_WHEN_COPYING_END
StatefulWidget: Needed because the list of tasks will change.
initState: Where you start the asynchronous data fetching.
FutureBuilder: Builds the UI based on the state of the _tasksFuture. It shows a loading indicator, error message, or the actual list based on snapshot.connectionState, snapshot.hasError, snapshot.hasData.
ListView.builder: Efficiently builds a scrollable list of widgets (one ListTile per task).
ListTile: A standard Flutter widget for list items with leading, title, subtitle, and trailing sections.
Checkbox onChanged: Demonstrates handling user interaction and calling the apiService.updateTask. Notice how setState is not directly called here after the API call, but refreshTaskList is called in the .then() block of the Future, which does call setState to rebuild the UI with potentially updated data.
Delete Button: Similar pattern to update, calling deleteTask and then refreshing the list.
FloatingActionButton: Placeholder for navigating to the add task screen.
Create Add/Edit Task Screen: A simple form to create or update a task.
Create lib/screens/task_form_screen.dart:
import 'package:flutter/material.dart'; import '../models/task.dart'; import '../services/api_service.dart'; class TaskFormScreen extends StatefulWidget { final Task? task; // Optional task to edit const TaskFormScreen({super.key, this.task}); @override _TaskFormScreenState createState() => _TaskFormScreenState(); } class _TaskFormScreenState extends State<TaskFormScreen> { final _formKey = GlobalKey<FormState>(); final _titleController = TextEditingController(); final _descriptionController = TextEditingController(); bool _isCompleted = false; // For editing, initial value comes from task final ApiService _apiService = ApiService(); @override void initState() { super.initState(); // If editing, populate fields with existing task data if (widget.task != null) { _titleController.text = widget.task!.title; _descriptionController.text = widget.task!.description ?? ''; _isCompleted = widget.task!.isCompleted; } } @override void dispose() { // Clean up controllers when the widget is removed _titleController.dispose(); _descriptionController.dispose(); super.dispose(); } void _saveTask() async { if (_formKey.currentState!.validate()) { // Form is valid, process data. final title = _titleController.text; final description = _descriptionController.text.isEmpty ? null : _descriptionController.text; if (widget.task == null) { // --- Create New Task --- final newTask = Task( id: 0, // Dummy ID for creation (backend assigns real one) title: title, description: description, isCompleted: false, // New tasks are not completed initially createdAt: DateTime.now(), // Dummy time ); try { await _apiService.createTask(newTask); // Success! Navigate back Navigator.pop(context, true); // Pass true to indicate success and potentially refresh list } catch (e) { // Handle error (e.g., show a Snackbar) print('Error creating task: $e'); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Failed to create task')), ); } } else { // --- Update Existing Task --- final updatedTask = Task( id: widget.task!.id, // Use the real ID title: title, description: description, isCompleted: _isCompleted, // Use the value from the state createdAt: widget.task!.createdAt, // Keep original time ); try { await _apiService.updateTask(widget.task!.id, updatedTask); // Success! Navigate back Navigator.pop(context, true); // Pass true to indicate success } catch (e) { // Handle error print('Error updating task: $e'); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Failed to update task')), ); } } } } @override Widget build(BuildContext context) { final isEditing = widget.task != null; return Scaffold( appBar: AppBar( title: Text(isEditing ? 'Edit Task' : 'Add New Task'), ), body: Padding( padding: const EdgeInsets.all(16.0), child: Form( // Form widget for validation key: _formKey, child: ListView( // Use ListView for scrollability children: <Widget>[ TextFormField( controller: _titleController, decoration: const InputDecoration(labelText: 'Title'), validator: (value) { // Add validation if (value == null || value.isEmpty) { return 'Please enter a title'; } return null; }, ), TextFormField( controller: _descriptionController, decoration: const InputDecoration(labelText: 'Description'), maxLines: 3, ), if (isEditing) // Show completion status only when editing Row( children: [ const Text('Completed:'), Checkbox( value: _isCompleted, onChanged: (bool? value) { if (value != null) { setState(() { _isCompleted = value; }); } }, ), ], ), const SizedBox(height: 20), ElevatedButton( onPressed: _saveTask, // Call save function child: Text(isEditing ? 'Update Task' : 'Create Task'), ), ], ), ), ), ); } }
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. Dart
IGNORE_WHEN_COPYING_END
StatefulWidget: Needed because the form fields hold state.
GlobalKey<FormState>: Used to access the state of the Form widget to validate it.
TextEditingController: Used to get and set text in TextFormField widgets. Remember to dispose() them.
initState: Populates the controllers if a task is passed in (indicating edit mode).
_saveTask: Handles both create and update logic based on whether widget.task is null. Calls the appropriate API service method.
Form and TextFormField with validator: Standard way to create forms with input validation in Flutter.
Navigator.pop(context, true): Navigates back to the previous screen (TaskListScreen) and optionally passes a result (true here, could signal a successful save).
Integrate Navigation: Connect the screens.
In lib/main.dart, update the onPressed of the FloatingActionButton in _TaskListScreenState:
floatingActionButton: FloatingActionButton( onPressed: () async { // Navigate to the AddTaskScreen final result = await Navigator.push( context, MaterialPageRoute(builder: (context) => TaskFormScreen()), ); // If the form screen returned true (meaning a save happened), refresh the list if (result == true) { _refreshTaskList(); } }, tooltip: 'Add Task', child: const Icon(Icons.add), ),
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. Dart
IGNORE_WHEN_COPYING_END
Update the onTap of the ListTile in _TaskListScreenState to navigate to the edit form:
onTap: () async { final result = await Navigator.push( context, MaterialPageRoute(builder: (context) => TaskFormScreen(task: task)), // Pass the task object ); // If the form screen returned true (meaning an update happened), refresh the list if (result == true) { _refreshTaskList(); } },
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. Dart
IGNORE_WHEN_COPYING_END
Run the Frontend:
Make sure your FastAPI backend is running (uvicorn main:app --reload).
In your terminal, navigate to the task_app_frontend folder.
Run:
flutter run
IGNORE_WHEN_COPYING_START
content_copy download
Use code with caution. Bash
IGNORE_WHEN_COPYING_END
Choose an emulator, simulator, or connected device.
You should now see the list of tasks (fetched from your backend!), be able to add new tasks, mark them as complete (by tapping the checkbox), and delete them.
Connecting the Pieces (Crucial for Full-Stack Understanding):
The interaction between Flutter and FastAPI happens via HTTP requests carrying data formatted as JSON.
Flutter -> FastAPI:
Flutter creates a Dart object (like TaskCreate data).
Uses jsonEncode to convert the Dart object into a JSON string.
Uses the http package (post, put, etc.) to send this JSON string in the request body to a specific FastAPI endpoint (/tasks/, /tasks/{id}).
Specifies the Content-Type: application/json header so FastAPI knows how to interpret the body.
FastAPI -> Flutter:
FastAPI receives the HTTP request.
If it's a POST/PUT, it uses Pydantic (schemas.TaskCreate, schemas.TaskUpdate) to automatically validate the incoming JSON body and convert it into a Python object.
Performs database operations using SQLAlchemy (crud.py).
Gets results (e.g., a list of SQLAlchemy Task objects).
Uses Pydantic (response_model=list[schemas.Task], response_model=schemas.Task) to automatically convert the SQLAlchemy objects into Python dictionaries that match the defined schema.
FastAPI serializes these dictionaries into a JSON string.
Sends the JSON string back in the HTTP response body.
Flutter Processing Response:
Flutter receives the HTTP response.
Checks the status code (e.g., 200).
Uses jsonDecode to convert the JSON string response body into a Dart map or list of maps.
Uses the fromJson factory constructor on your Task model to convert the Dart map(s) into Dart Task objects you can work with in the UI.
Understanding this request/response cycle, the role of JSON as the data interchange format, and how libraries like Pydantic and dart:convert handle the conversion between native objects and JSON is absolutely essential for full-stack development.
Essential Elements & Importance Recap for a Fresher:
Flutter:
Widgets & Widget Tree: Visual building blocks. Importance: How you structure and display everything the user sees.
State Management (setState, FutureBuilder): Making the UI dynamic. Importance: Updating the UI when data changes (like tasks loading or being modified).
Async Programming (Future, async, await): Handling operations that take time (like network calls) without freezing the app. Importance: Keeping your app responsive while waiting for data.
HTTP Requests (http package): The bridge to the backend. Importance: How your frontend talks to the API to get and send data.
JSON Serialization/Deserialization (dart:convert): Converting data between Dart objects and JSON format. Importance: Understanding the format used for communication between frontend and backend.
FastAPI:
Routing & HTTP Methods: Defining the API structure and available actions. Importance: How the frontend knows what URL to call and what method (GET, POST, etc.) to use for different operations.
Pydantic Models (Schemas): Data validation and structuring for requests and responses. Importance: Ensures data sent/received is in the correct format, catches errors early, and provides automatic documentation (Swagger UI).
SQLAlchemy (ORM): Interacting with the database using Python objects. Importance: Abstracts away raw SQL, making database operations safer and more Pythonic.
Dependency Injection (Depends): Providing necessary components (like DB sessions) to route functions. Importance: Makes your code modular, easier to test, and handles resource management (like closing DB connections).
MySQL:
Database & Table Design: Organizing data into a logical structure. Importance: How data is stored persistently, ensuring consistency and relationships (though simple here).
SQL Basics: The language to interact with the database. Importance: The fundamental skill for retrieving, inserting, updating, and deleting data directly in the database.
Integration:
- HTTP & JSON: The communication protocol and data format between layers. Importance: The glue that holds the full stack together. Understanding how data flows is key.
Next Steps & Enhancements:
This is a solid foundation! Here's what you could explore next:
Error Handling: More robust error handling in both frontend and backend (e.g., showing user-friendly messages in Flutter if an API call fails).
Input Validation: Add more validation in the Flutter form (e.g., minimum length for title) and rely on FastAPI's Pydantic validation as the primary backend defense.
State Management: For larger Flutter apps, look into more advanced state management solutions (Provider, Riverpod, BLoC, MobX) instead of just setState.
Authentication & Authorization: Add users, login, registration, and restrict access to tasks based on the logged-in user. This introduces concepts like hashing passwords, JWTs (JSON Web Tokens), and securing API endpoints.
More Features: Add due dates, categories, task assignment (requires relationships in the DB), sorting, filtering, search.
Testing: Write unit and integration tests for your backend and frontend code.
Deployment: Learn how to deploy your FastAPI app (e.g., to Heroku, Render, a VPS) and your Flutter app (to app stores, web hosting).
Database Migrations: Use a tool like Alembic (for SQLAlchemy) to manage database schema changes over time.
Important Security Note: This simple app has no authentication or authorization. Do not use this code directly for anything requiring security or handling sensitive data without adding proper security measures!
Building this project should give you a strong practical understanding of how the different layers of a full-stack application work together. Don't be discouraged if you encounter errors – debugging is a massive part of development. Use print statements, IDE debuggers, and online resources (like Stack Overflow, official docs) frequently. 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

Singaraju Saiteja
Singaraju Saiteja
I am an aspiring mobile developer, with current skill being in flutter.