Project Planning & Clean Architecture Setup
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
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.
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/
^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.yamlwith explicit version constraints - The
Userentity lives indomain/and is completely free of JSON or Flutter imports; theUserModelindata/extends it and handles serialisation