Clean Architecture in Flutter: Folder Structure and Layers for Large Projects

As an app grows, code falls apart without structure. This article explains feature-first folders, UI, state, repository and data-source layers and the principles of testability in plain language.

3 min read1 views
Clean Architecture in Flutter: Folder Structure and Layers for Large Projects

Small apps fit in a few files. But when an app grows and several people work on it, every simple change becomes risky without structure. Clean architecture means every part of the code has one clear responsibility.

#Folder structure: feature-first

Instead of grouping by file type (all widgets together, all models together), group by feature:

lib/
  core/            # shared: network, theme, utils
  features/
    auth/
      data/        # api + repository implementation
      domain/      # models + repository interface
      presentation/# screens, widgets, controllers
    courses/
      data/
      domain/
      presentation/
  main.dart

The benefit: to work on a feature you look in one folder, and removing or moving a feature is easy.

#Four layers with clear responsibilities

  • UI (widgets): only displays and hands user events to the controller. No business logic.
  • State / controller: holds the screen's state and gets data from the repository (Riverpod, Bloc or ChangeNotifier).
  • Repository: the single gateway to data for the rest of the app. It does not know whether data comes from the network or a local database.
  • Data source: the actual work with the API or local storage.

#A repository behind an interface

abstract class CourseRepository {
  Future<List<Course>> fetchCourses();
}

class ApiCourseRepository implements CourseRepository {
  ApiCourseRepository(this._api);
  final CourseApi _api;

  @override
  Future<List<Course>> fetchCourses() => _api.getCourses();
}

Why an interface? In tests you plug in a FakeCourseRepository and test screen behaviour without internet.

#Dependency injection (DI)

Classes should not build their own dependencies; they receive them. Through constructors or Riverpod:

final courseRepositoryProvider = Provider<CourseRepository>(
  (ref) => ApiCourseRepository(ref.watch(courseApiProvider)),
);

#Immutable models

Declare models with final fields and const constructors, and create a new version to change something (copyWith). This eliminates bugs caused by hidden mutation.

#Rules that keep the structure healthy

  • Widgets never call HTTP directly.
  • A lower layer never imports a higher one (domain knows nothing about presentation).
  • Every file has one reason to change.

#Do not overdo it

A three-screen app does not need four layers. Start simple and add layers when you feel the pain. Architecture should give you speed, not slow you down.

If you take away one thing: separate UI from logic and data. Everything else starts there.

Learn Flutter the professional, project-based way

The FastFlutter VIP course takes you from zero to a published app, with direct support from the instructor.

View the VIP course