Flutter Lesson 16: Project Practice: Comprehensive Application Development (Part 1)



This content originally appeared on DEV Community and was authored by Ge Ji

today we’ll be building a to-do list app to put our Flutter core knowledge into practice. As a classic introductory project, a to-do list covers key concepts like page navigation, data display, and basic interactions, making it perfect for reinforcing what we’ve learned so far.

I. Requirements Analysis and Page Design

1. Core Requirements Breakdown

The core goal of our to-do list app is to help users manage daily tasks. The key features we need to implement include:

  • Displaying all to-do tasks (categorized by completed/uncompleted status)
  • Viewing detailed information of individual tasks
  • Adding new tasks
  • Editing existing tasks
  • Marking task completion status
  • Deleting tasks
  • Basic settings (such as theme switch, about page, etc.)

2. Page Architecture Design

Based on the above requirements, we’ll design three core pages:

  • Home Page (Task List Page): As the app’s entry point, it will display a title and an add button at the top, with a list in the middle showing all tasks. It supports pull-to-refresh and swipe-to-delete operations. List items will show task titles, creation times, and completion status markers.
  • Detail Page: Accessed by clicking list items, it will display complete task information (title, content, creation time, deadline, etc.) with edit and delete buttons at the bottom.
  • Settings Page: Accessed via the settings icon on the home page, it will contain switch components (like notification toggle), an “About Us” entry, and a clear cache button.

3. Prototype Sketch Concept

The home page will use the classic “AppBar+ListView” structure, with a “+” icon button on the right side of the AppBar for adding tasks. The detail page will use a ScrollView to avoid content overflow, with an AppBar at the top providing a back button. The settings page will use ListView.builder to construct a list of settings items, where each item consists of an icon, text, and an operation component (switch/arrow).


II. Project Architecture Setup: Directory Structure

A reasonable directory structure makes the project more maintainable. We’ll use the following organization:

1. pages Directory

Stores all complete pages, with each page in its own folder containing the corresponding Dart file. For example:

  • home_page: Code related to the home page
  • detail_page: Code related to the detail page
  • setting_page: Code related to the settings page

2. widgets Directory

Stores reusable custom components, with subdirectories categorized by function:

  • common: General components (like custom buttons, input fields)
  • task: Task-related components (like task list items, task status labels)

3. utils Directory

Stores utility classes and helper methods:

  • router.dart: Routing management tool
  • date_utils.dart: Date processing tool
  • storage_utils.dart: Local storage tool (to be implemented in detail in the next lesson)

4. models Directory

Stores data model classes, using Dart classes to define data structures:

// models/task_model.dart
class Task {
  final String id; // Unique task identifier
  String title; // Task title
  String content; // Task content
  DateTime createTime; // Creation time
  DateTime? deadline; // Deadline (optional)
  bool isCompleted; // Completion status

  Task({
    required this.id,
    required this.title,
    this.content = '',
    DateTime? createTime,
    this.deadline,
    this.isCompleted = false,
  }) : createTime = createTime ?? DateTime.now();
}

III. Implementation of Core Utility Classes

1. Date Processing Utility (utils/date_utils.dart)

import 'package:intl/intl.dart';

class DateUtils {
  /// Formats date to "yyyy-MM-dd"
  static String formatDate(DateTime date) {
    return DateFormat('yyyy-MM-dd').format(date);
  }

  /// Formats date to "yyyy-MM-dd HH:mm"
  static String formatDateTime(DateTime date) {
    return DateFormat('yyyy-MM-dd HH:mm').format(date);
  }

  /// Formats date to friendly display (e.g., "Today 14:30", "Yesterday 10:15")
  static String formatFriendly(DateTime date) {
    final now = DateTime.now();
    final today = DateTime(now.year, now.month, now.day);
    final yesterday = today.subtract(const Duration(days: 1));
    final dateOnly = DateTime(date.year, date.month, date.day);

    if (dateOnly == today) {
      return 'Today ${DateFormat('HH:mm').format(date)}';
    } else if (dateOnly == yesterday) {
      return 'Yesterday ${DateFormat('HH:mm').format(date)}';
    } else if (date.year == now.year) {
      return DateFormat('MM-dd HH:mm').format(date);
    } else {
      return DateFormat('yyyy-MM-dd HH:mm').format(date);
    }
  }
}

Before using, add the dependency to pubspec.yaml:

dependencies:
  intl: ^0.20.2

2. Routing Management Utility (utils/router.dart)

import 'package:flutter/material.dart';
import '../pages/home_page.dart';
import '../pages/detail_page.dart';
import '../pages/setting_page.dart';

class Router {
  // Route name constants
  static const String home = '/home';
  static const String detail = '/detail';
  static const String setting = '/setting';

  // Route configuration
  static Map<String, WidgetBuilder> routes = {
    home: (context) => const HomePage(),
    setting: (context) => const SettingPage(),
    // Detail page needs dynamic parameters, handled during navigation
  };

  // Generate routes
  static Route<dynamic> generateRoute(RouteSettings settings) {
    switch (settings.name) {
      case detail:
        final args = settings.arguments as Map<String, dynamic>?;
        return MaterialPageRoute(
          builder: (context) => DetailPage(
            task: args?['task'],
            onSave: args?['onSave'],
          ),
        );
      default:
        return MaterialPageRoute(
          builder: (context) => const Scaffold(
            body: Center(child: Text('Page not found')),
          ),
        );
    }
  }
}

3. Application Entry (main.dart)

import 'package:flutter/material.dart' hide Router;
import 'pages/home_page.dart';
import 'utils/router.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'To-Do List',
      // App theme configuration
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
        // Configure global text styles
        textTheme: const TextTheme(
          bodyLarge: TextStyle(fontSize: 16),
          bodyMedium: TextStyle(fontSize: 14),
          titleLarge: TextStyle(
            fontSize: 20,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
      // Disable debug mode banner
      debugShowCheckedModeBanner: false,
      // Initial route
      initialRoute: Router.home,
      // Route table
      routes: Router.routes,
      // Generate dynamic routes
      onGenerateRoute: Router.generateRoute,
      // Home page
      home: const HomePage(),
    );
  }
}

IV. Implementation of Basic Pages

1. Home Page Implementation (pages/home_page.dart)

The home page is the core entry point of the app, where we’ll implement task list display, an add task button, and a settings entry.

import 'package:flutter/material.dart';
import '../widgets/task/task_item.dart';
import '../models/task_model.dart';
import 'detail_page.dart';
import 'setting_page.dart';

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  // Mock task data
  final List<Task> _tasks = [
    Task(
      id: '1',
      title: 'Learn Flutter',
      content: 'Complete the practical project in Lesson 16',
      deadline: DateTime.now().add(const Duration(days: 1)),
    ),
    Task(
      id: '2',
      title: 'Buy groceries',
      content: 'Milk, bread, fruits',
      isCompleted: true,
    ),
  ];

  // Navigate to detail page
  void _navigateToDetail({Task? task}) {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => DetailPage(
          task: task,
          onSave: (newTask) {
            setState(() {
              if (task == null) {
                // Add new task
                _tasks.add(newTask);
              } else {
                // Edit existing task
                final index = _tasks.indexWhere((t) => t.id == task.id);
                if (index != -1) {
                  _tasks[index] = newTask;
                }
              }
            });
          },
        ),
      ),
    );
  }

  // Toggle task completion status
  void _toggleTaskStatus(String id) {
    setState(() {
      final task = _tasks.firstWhere((t) => t.id == id);
      task.isCompleted = !task.isCompleted;
    });
  }

  // Delete task
  void _deleteTask(String id) {
    setState(() {
      _tasks.removeWhere((t) => t.id == id);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('To-Do List'),
        actions: [
          // Settings button
          IconButton(
            icon: const Icon(Icons.settings),
            onPressed: () {
              Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => const SettingPage()),
              );
            },
          ),
        ],
      ),
      body: _tasks.isEmpty
          ? const Center(child: Text('No tasks yet, add one!'))
          : ListView.builder(
              padding: const EdgeInsets.all(16),
              itemCount: _tasks.length,
              itemBuilder: (context, index) {
                final task = _tasks[index];
                return TaskItem(
                  task: task,
                  onTap: () => _navigateToDetail(task: task),
                  onStatusChanged: _toggleTaskStatus,
                  onDelete: _deleteTask,
                );
              },
            ),
      // Add task button
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () => _navigateToDetail(),
      ),
    );
  }
}

2. Detail Page Implementation (pages/detail_page.dart)

The detail page handles task viewing, editing, and deletion, requiring form input and data transmission.

import 'package:flutter/material.dart' hide DateUtils;
import '../models/task_model.dart';
import '../utils/date_utils.dart';

class DetailPage extends StatefulWidget {
  final Task? task;
  final Function(Task) onSave;

  const DetailPage({
    super.key,
    this.task,
    required this.onSave,
  });

  @override
  State<DetailPage> createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
  late final TextEditingController _titleController;
  late final TextEditingController _contentController;
  DateTime? _deadline;
  bool _isCompleted = false;

  @override
  void initState() {
    super.initState();
    // Initialize form data (edit mode)
    if (widget.task != null) {
      _titleController = TextEditingController(text: widget.task!.title);
      _contentController = TextEditingController(text: widget.task!.content);
      _deadline = widget.task!.deadline;
      _isCompleted = widget.task!.isCompleted;
    } else {
      // Add mode
      _titleController = TextEditingController();
      _contentController = TextEditingController();
    }
  }

  @override
  void dispose() {
    _titleController.dispose();
    _contentController.dispose();
    super.dispose();
  }

  // Save task
  void _saveTask() {
    if (_titleController.text.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Please enter a task title')),
      );
      return;
    }

    final task = Task(
      id: widget.task?.id ?? DateTime.now().microsecondsSinceEpoch.toString(),
      title: _titleController.text,
      content: _contentController.text,
      createTime: widget.task?.createTime ?? DateTime.now(),
      deadline: _deadline,
      isCompleted: _isCompleted,
    );

    widget.onSave(task);
    Navigator.pop(context);
  }

  // Select deadline
  Future<void> _selectDate() async {
    final picked = await showDatePicker(
      context: context,
      initialDate: _deadline ?? DateTime.now(),
      firstDate: DateTime.now(),
      lastDate: DateTime.now().add(const Duration(days: 365)),
    );
    if (picked != null) {
      setState(() => _deadline = picked);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.task == null ? 'Add Task' : 'Edit Task'),
        actions: [
          // Delete button (only shown in edit mode)
          if (widget.task != null)
            IconButton(
              icon: const Icon(Icons.delete, color: Colors.red),
              onPressed: () {
                Navigator.pop(context);
                // You can add a delete confirmation dialog here
              },
            ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: ListView(
          children: [
            // Task title input
            TextField(
              controller: _titleController,
              decoration: const InputDecoration(
                labelText: 'Task Title',
                border: OutlineInputBorder(),
              ),
              style: const TextStyle(fontSize: 18),
            ),
            const SizedBox(height: 16),
            // Task content input
            TextField(
              controller: _contentController,
              decoration: const InputDecoration(
                labelText: 'Task Content',
                border: OutlineInputBorder(),
              ),
              maxLines: 4,
            ),
            const SizedBox(height: 16),
            // Deadline selection
            ListTile(
              title: const Text('Deadline'),
              subtitle: Text(_deadline != null
                  ? DateUtils.formatDate(_deadline!)
                  : 'Not set'),
              trailing: const Icon(Icons.calendar_today),
              onTap: _selectDate,
            ),
            // Completion status toggle
            SwitchListTile(
              title: const Text('Completion Status'),
              value: _isCompleted,
              onChanged: (value) => setState(() => _isCompleted = value),
            ),
            const SizedBox(height: 20),
            // Save button
            ElevatedButton(
              onPressed: _saveTask,
              child: const Padding(
                padding: EdgeInsets.all(12),
                child: Text('Save Task'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

3. Settings Page Implementation (pages/setting_page.dart)

The settings page provides basic configuration options for the app, displayed as a list of settings items.

import 'package:flutter/material.dart';

class SettingPage extends StatelessWidget {
  const SettingPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Settings'),
      ),
      body: ListView(
        children: [
          // Notification settings
          SwitchListTile(
            title: const Text('Enable Notification Reminders'),
            value: true, // Simulated enabled state
            onChanged: (value) {
              // Actual logic will be implemented in the state management section
            },
          ),
          // Dark mode settings (to be implemented in next lesson)
          ListTile(
            title: const Text('Dark Mode'),
            trailing: const Icon(Icons.arrow_forward_ios),
            onTap: () {
              // Navigate to dark mode settings page
            },
          ),
          // About us
          ListTile(
            title: const Text('About Us'),
            trailing: const Icon(Icons.arrow_forward_ios),
            onTap: () {
              showAboutDialog(
                context: context,
                applicationName: 'To-Do List',
                applicationVersion: '1.0.0',
                children: [
                  const Padding(
                    padding: EdgeInsets.only(top: 16),
                    child: Text('A simple and efficient task management tool to help you better plan your life and work.'),
                  ),
                ],
              );
            },
          ),
          // Clear cache
          ListTile(
            title: const Text('Clear Cache'),
            trailing: const Icon(Icons.delete),
            onTap: () {
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('Cache cleared')),
              );
            },
          ),
        ],
      ),
    );
  }
}

V. Custom Component Implementation (widgets/task/task_item.dart)

Implementing the task list item component to improve code reusability:

import 'package:flutter/material.dart';
import '../../models/task_model.dart';
import '../../utils/date_utils.dart';

class TaskItem extends StatelessWidget {
  final Task task;
  final VoidCallback onTap;
  final Function(String) onStatusChanged;
  final Function(String) onDelete;

  const TaskItem({
    super.key,
    required this.task,
    required this.onTap,
    required this.onStatusChanged,
    required this.onDelete,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 2,
      margin: const EdgeInsets.only(bottom: 12),
      child: ListTile(
        onTap: onTap,
        leading: Checkbox(
          value: task.isCompleted,
          onChanged: (value) => onStatusChanged(task.id),
        ),
        title: Text(
          task.title,
          style: TextStyle(
            decoration: task.isCompleted ? TextDecoration.lineThrough : null,
            color: task.isCompleted ? Colors.grey : null,
          ),
        ),
        subtitle: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            if (task.deadline != null)
              Text(
                'Deadline: ${DateUtils.formatDate(task.deadline!)}',
                style: const TextStyle(fontSize: 12, color: Colors.grey),
              ),
            Text(
              'Created: ${DateUtils.formatDate(task.createTime)}',
              style: const TextStyle(fontSize: 12, color: Colors.grey),
            ),
          ],
        ),
        // Right arrow icon
        trailing: const Icon(Icons.arrow_forward_ios, size: 18),
        onLongPress: () => onDelete(task.id),
      ),
    );
  }
}


This content originally appeared on DEV Community and was authored by Ge Ji