Capstone: Real-World Flutter Project

Project Planning & Clean Architecture Setup

15 min Lesson 1 of 10

Project Planning & Clean Architecture Setup

Before writing a single line of Flutter code, the most important investment you can make is a well-defined project plan paired with a solid architectural foundation. In this capstone project we will build a full-featured Task Manager app. This lesson covers how to scope the feature set, define the folder structure following Clean Architecture principles, and configure all required dependencies in pubspec.yaml.

1. Defining the Feature Scope

A clear feature scope prevents scope creep and keeps the codebase focused. For our Task Manager capstone the agreed feature set is:

  • Authentication — email/password sign-in and sign-up (local + remote)
  • Task CRUD — create, read, update, and delete tasks with title, description, due date, and priority
  • Categories — group tasks under user-defined categories
  • Offline-first — tasks cached locally with SQLite; synced to a REST API when online
  • Notifications — local push notifications for due tasks
  • Theme switching — light and dark mode persisted across sessions
Note: Writing the feature scope in a plain text file (e.g. docs/features.md) at the start of the project is a professional habit. It acts as the contract between planning and implementation and helps you decide where each piece of code belongs architecturally.

2. Clean Architecture — Three Layers

Clean Architecture (popularised by Robert C. Martin) organises code into concentric layers with strict dependency rules: inner layers know nothing about outer layers. For Flutter apps the three practical layers are:

  • Domain — pure business logic; entities, use-cases, and abstract repository interfaces. Zero Flutter imports.
  • Data — concrete implementations of repositories; remote data sources (API), local data sources (SQLite/SharedPreferences), and data models (JSON ↔ entity mapping).
  • Presentation — Flutter widgets, pages, state management (Riverpod providers/notifiers). Depends on domain use-cases, never on data directly.
Tip: A useful test: if you can run your domain layer as a plain Dart command-line program with no Flutter SDK, you have the separation right. Use-cases should never call BuildContext or import any Flutter package.

3. Folder Structure

The recommended folder layout for our capstone inside lib/ is:

Clean Architecture Folder Layout

lib/
├── core/
│   ├── error/
│   │   ├── exceptions.dart       // AppException, NetworkException, etc.
│   │   └── failures.dart         // Failure sealed class for Result type
│   ├── network/
│   │   └── network_info.dart     // Abstract NetworkInfo interface
│   ├── theme/
│   │   └── app_theme.dart        // ThemeData light & dark
│   └── utils/
│       └── date_formatter.dart   // Pure helper functions
│
├── features/
│   ├── auth/
│   │   ├── data/
│   │   │   ├── datasources/
│   │   │   │   ├── auth_local_datasource.dart
│   │   │   │   └── auth_remote_datasource.dart
│   │   │   ├── models/
│   │   │   │   └── user_model.dart     // toJson / fromJson
│   │   │   └── repositories/
│   │   │       └── auth_repository_impl.dart
│   │   ├── domain/
│   │   │   ├── entities/
│   │   │   │   └── user.dart           // Plain Dart class — no JSON
│   │   │   ├── repositories/
│   │   │   │   └── auth_repository.dart // Abstract interface
│   │   │   └── usecases/
│   │   │       ├── sign_in_usecase.dart
│   │   │       └── sign_up_usecase.dart
│   │   └── presentation/
│   │       ├── pages/
│   │       │   ├── login_page.dart
│   │       │   └── register_page.dart
│   │       ├── providers/
│   │       │   └── auth_provider.dart
│   │       └── widgets/
│   │           └── auth_form_field.dart
│   │
│   └── tasks/
│       ├── data/ ...
│       ├── domain/ ...
│       └── presentation/ ...
│
└── main.dart

Every feature is a self-contained vertical slice. Adding a new feature means creating a new folder under features/ — existing features are never modified.

4. Configuring pubspec.yaml

All dependencies are declared up front so the whole team installs the same versions. Below is the complete pubspec.yaml for the capstone project:

pubspec.yaml — Full Dependency Manifest

name: task_manager
description: Capstone Task Manager — offline-first Flutter app
publish_to: none
version: 1.0.0+1

environment:
  sdk: ">=3.3.0 <4.0.0"

dependencies:
  flutter:
    sdk: flutter

  # State management
  flutter_riverpod: ^2.5.1
  riverpod_annotation: ^2.3.5

  # Navigation
  go_router: ^13.2.0

  # Local storage
  sqflite: ^2.3.2
  shared_preferences: ^2.2.3
  path_provider: ^2.1.3
  path: ^1.9.0

  # Networking
  dio: ^5.4.3
  connectivity_plus: ^6.0.3

  # Serialisation
  freezed_annotation: ^2.4.1
  json_annotation: ^4.9.0

  # Local notifications
  flutter_local_notifications: ^17.2.2

  # Utilities
  dartz: ^0.10.1       # Either / Option functional types
  equatable: ^2.0.5
  intl: ^0.19.0
  uuid: ^4.4.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^2.4.9
  freezed: ^2.5.2
  json_serializable: ^6.8.0
  riverpod_generator: ^2.4.3
  flutter_lints: ^4.0.0
  mockito: ^5.4.4
  bloc_test: ^9.1.7

flutter:
  uses-material-design: true
  assets:
    - assets/images/
    - assets/icons/
Warning: Always pin to a specific minor version (e.g. ^2.5.1) rather than using any or omitting the constraint. Unpinned dependencies can break your build when a package publishes a breaking change. Run flutter pub upgrade --major-versions deliberately, not accidentally.

5. The Dependency Rule in Practice

The single most important rule in Clean Architecture is the Dependency Rule: source code dependencies point only inward. Consider the User entity and its related classes:

Domain Entity vs Data Model

// domain/entities/user.dart  — NO external imports
class User {
  final String id;
  final String email;
  final String displayName;

  const User({
    required this.id,
    required this.email,
    required this.displayName,
  });
}

// data/models/user_model.dart  — depends on domain entity, NOT vice-versa
import 'package:task_manager/features/auth/domain/entities/user.dart';

class UserModel extends User {
  const UserModel({
    required super.id,
    required super.email,
    required super.displayName,
  });

  factory UserModel.fromJson(Map<String, dynamic> json) {
    return UserModel(
      id: json['id'] as String,
      email: json['email'] as String,
      displayName: json['display_name'] as String,
    );
  }

  Map<String, dynamic> toJson() => {
    'id': id,
    'email': email,
    'display_name': displayName,
  };
}

6. Summary

A successful capstone starts with disciplined planning:

  • Write the feature scope before opening your IDE
  • Adopt Clean Architecture: Domain → Data → Presentation, dependencies pointing inward
  • Mirror the architecture in the folder structure so every file has an obvious home
  • Declare all dependencies in pubspec.yaml with explicit version constraints
  • The User entity lives in domain/ and is completely free of JSON or Flutter imports; the UserModel in data/ extends it and handles serialisation
Key Takeaway: Clean Architecture is not extra ceremony — it is the scaffolding that makes every subsequent lesson easier. When state management, navigation, API integration, and testing all follow the same layer contract, changes stay isolated and the codebase remains maintainable as it grows.