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?

  1. 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.

  2. 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.

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

  1. Python: Install Python 3.7+ (python.org).

  2. MySQL: Install MySQL Server and a client tool (like MySQL Workbench or DBeaver) (dev.mysql.com/downloads).

  3. Flutter SDK: Install Flutter (flutter.dev/docs/get-started/install).

  4. 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:

    1. Connect to MySQL: Use your client tool (MySQL Workbench, DBeaver, or the command line).

    2. 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

    3. Use the Database:

             USE task_app_db;
      

      IGNORE_WHEN_COPYING_START

      content_copy download

      Use code with caution. SQL

      IGNORE_WHEN_COPYING_END

    4. 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.

    5. (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:

    1. 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)

    2. 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

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

    4. 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).

    5. 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.

    6. 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.

    7. 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).

    8. 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.

    9. 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:

    1. 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).

    2. 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.

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

    4. 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).

    5. 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.

    6. 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).

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

    8. 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:

  1. Error Handling: More robust error handling in both frontend and backend (e.g., showing user-friendly messages in Flutter if an API call fails).

  2. 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.

  3. State Management: For larger Flutter apps, look into more advanced state management solutions (Provider, Riverpod, BLoC, MobX) instead of just setState.

  4. 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.

  5. More Features: Add due dates, categories, task assignment (requires relationships in the DB), sorting, filtering, search.

  6. Testing: Write unit and integration tests for your backend and frontend code.

  7. Deployment: Learn how to deploy your FastAPI app (e.g., to Heroku, Render, a VPS) and your Flutter app (to app stores, web hosting).

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

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

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