clean_architecture_kit 2.0.1
clean_architecture_kit: ^2.0.1 copied to clipboard
An opinionated linter for enforcing Clean Architecture in Dart & Flutter. Provides real-time warnings and intelligent quick fixes to automate boilerplate.
Clean Architecture Kit #
An opinionated and automated linter for enforcing a strict Clean Architecture in Dart & Flutter projects. It not only finds architectural violations but provides powerful quick fixes to generate your boilerplate for you.
Features #
- ✅ Architectural Guardrails: Automatically detect when you import from a wrong layer, use a data Model in the domain, or depend on a concrete implementation instead of an abstraction.
- 🚀 Intelligent Quick Fixes: Go beyond just finding problems. The linter can generate boilerplate code for you, such as creating
UseCaseclasses andtoEntity()mapping methods. - 📦 Works Out-of-the-Box: Integrates seamlessly with the
clean_architecture_corepackage, providing a zero-configuration experience for base classes. - 🔧 Highly Configurable: Customize everything from folder names to class naming conventions to fit your team's style guide. Supports both "feature-first" and "layer-first" project structures.
Getting Started #
1. Add Dependencies #
Add clean_architecture_kit and custom_lint to your dev_dependencies. For the best out-of-the-box experience, also add clean_architecture_core.
# pubspec.yaml
dependencies:
# Your other dependencies...
fpdart: ^1.1.0 # Recommended for Either type
clean_architecture_core: ^1.0.0 # Provides default base classes
dev_dependencies:
# Your other dev_dependencies...
custom_lint: ^0.6.4 # Or latest version
clean_architecture_kit: ^1.0.0 # Or latest version
2. Configure analysis_options.yaml #
Create or update your analysis_options.yaml file. Start with this minimal configuration:
# analysis_options.yaml
analyzer:
plugins:
# IMPORTANT: Only add custom_lint here.
# clean_architecture_kit is discovered automatically.
- custom_lint
custom_lint:
rules:
# --- Enable all the rules you want from the list below ---
- disallow_model_in_domain: true
- enforce_layer_independence: true
- enforce_naming_conventions: true
- missing_use_case: true
# ... and so on for all rules you wish to enable.
# --- Provide the shared configuration for the plugin ---
- clean_architecture:
project_structure: 'feature_first'
feature_first_paths:
features_root: "features"
layer_definitions:
domain:
entities: ['entities']
repositories: ['contracts']
use_cases: ['use_cases']
data:
models: ['models']
repositories: ['repositories']
data_sources: ['data_sources']
naming_conventions:
entity: '{{name}}Entity'
model: '{{name}}Model'
use_case: '{{name}}Usecase'
repository_interface: '{{name}}Repository'
repository_implementation: '{{name}}RepositoryImpl'
3. Restart the Analysis Server #
In your IDE, restart the Dart analysis server to activate the linter.
- VS Code: Open the Command Palette (
Ctrl+Shift+PorCmd+Shift+P) and runDart: Restart Analysis Server. - Android Studio/IntelliJ: Find the "Dart Analysis" tool window and click the restart icon.
Rules Overview #
| Rule | Quick Fix |
|---|---|
| Purity & Responsibility Rules | |
disallow_model_in_domain |
|
disallow_entity_in_data_source |
|
disallow_repository_in_presentation |
|
disallow_model_return_from_repository |
|
disallow_use_case_in_widget |
|
disallow_flutter_imports_in_domain |
|
disallow_flutter_types_in_domain |
|
enforce_model_to_entity_mapping |
✅ |
enforce_model_inherits_entity |
|
| Dependency & Structure Rules | |
enforce_layer_independence |
|
enforce_abstract_data_source_dependency |
|
enforce_file_and_folder_location |
|
| Naming, Type Safety & Inheritance | |
enforce_naming_conventions |
|
enforce_custom_return_type |
|
enforce_use_case_inheritance |
|
enforce_repository_inheritance |
|
| Code Generation | |
missing_use_case |
✅ |
Detailed Lint Rule Explanations #
disallow_model_in_domain #
- Purpose: To prevent data-layer Models from leaking into the Domain Layer.
- Description: This is the core data purity rule for the domain layer. It inspects all method signatures (return types, parameters) and fields and flags any type that matches the naming convention for a
Model, ensuring the domain only uses pureEntities.
✅ Good Example
// Domain layer file
abstract interface class AuthRepository {
// Correct: Uses a pure `UserEntity`.
FutureEither<UserEntity> getUser(String id);
}
❌ Bad Example
// Domain layer file
abstract interface class AuthRepository {
// VIOLATION: Uses a `UserModel` from the data layer.
FutureEither<UserModel> getUser(String id); // <-- LINT WARNING HERE
}
disallow_entity_in_data_source #
- Purpose: To prevent domain-layer Entities from leaking into the Data Source Layer.
- Description: Enforces that Data Sources (classes that fetch raw data from an API or database) speak in terms of raw data types or
Models, not pure domainEntities. The repository is responsible for the mapping.
✅ Good Example
// Data Source file
abstract interface class AuthRemoteDataSource {
// Correct: Returns a `UserModel`.
Future<UserModel> getUser(String id);
}
❌ Bad Example
// Data Source file
abstract interface class AuthRemoteDataSource {
// VIOLATION: Returns a pure `UserEntity`.
Future<UserEntity> getUser(String id); // <-- LINT WARNING HERE
}
disallow_repository_in_presentation #
- Purpose: To decouple the Presentation Layer from the data access implementation details.
- Description: Prevents presentation logic classes (like BLoCs, Cubits, or Providers) from depending directly on a
Repository. The presentation layer should only depend on specificUseCasesto execute business logic.
✅ Good Example
// Presentation manager file
class AuthBloc {
// Correct: Depends on a specific UseCase.
final GetUserUsecase _getUserUsecase;
AuthBloc(this._getUserUsecase);
}
❌ Bad Example
// Presentation manager file
class AuthBloc {
// VIOLATION: Depends on the entire repository.
final AuthRepository _repository;
AuthBloc(this._repository); // <-- LINT WARNING HERE
}
disallow_model_return_from_repository #
- Purpose: To ensure the Repository Implementation correctly maps data
Modelsto domainEntities. - Description: This lint checks the public methods in a
RepositoryImplclass. It enforces that the final return type is a pureEntity, guaranteeing that the mapping fromModeltoEntityhappens inside the repository before the data is returned to aUseCase.
✅ Good Example
// Repository implementation file
class AuthRepositoryImpl implements AuthRepository {
@override
FutureEither<UserEntity> getUser(String id) async {
// ... fetches userModel
return Right(userModel.toEntity()); // Correct: Returns an Entity.
}
}
❌ Bad Example
// Repository implementation file
class AuthRepositoryImpl implements AuthRepository {
@override
// VIOLATION: Method returns a `UserModel` instead of a `UserEntity`.
FutureEither<UserModel> getUser(String id) async { // <-- LINT WARNING HERE
return const Right(UserModel(id: '1'));
}
}
disallow_use_case_in_widget #
- Purpose: To keep UI components clean of business logic invocation.
- Description: Prevents UI widgets from directly calling a
UseCase. All business logic should be triggered from a presentation manager (BLoC, Cubit, Provider), which then calls theUseCaseand exposes the result as state to the UI.
✅ Good Example
// A widget that receives state from a BLoC/Provider.
class UserProfile extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Correct: Listens to a provider to get data.
final user = ref.watch(userProvider);
return Text(user.name);
}
}
❌ Bad Example
// A widget that calls a use case directly.
class UserProfile extends StatelessWidget {
final GetUserUsecase _usecase;
@override
Widget build(BuildContext context) {
return ElevatedButton(onPressed: () {
// VIOLATION: Calling a use case from a widget.
_usecase.call('123'); // <-- LINT WARNING HERE
});
}
}
disallow_flutter_imports_in_domain #
- Purpose: To ensure the Domain Layer is platform-agnostic.
- Description: Disallows any
import 'package:flutter/...'statement in any domain layer file, guaranteeing that your core business logic is pure Dart and can be tested without a Flutter environment.
✅ Good Example
````dart // domain/entities/user_entity.dart // Correct: No Flutter imports. import 'package:meta/meta.dart';class UserEntity { ... }
</details>
<details>
<summary>❌ Bad Example</summary>
````dart
// domain/entities/user_entity.dart
// VIOLATION: Importing a Flutter package.
import 'package:flutter/material.dart'; // <-- LINT WARNING HERE
class UserEntity { ... }
disallow_flutter_types_in_domain #
- Purpose: To prevent UI-specific data types from polluting the Domain Layer.
- Description: A companion to the rule above, this lint inspects method signatures and fields and disallows any types from the Flutter SDK, such as
Color,IconData, orWidget.
✅ Good Example
````dart // domain/entities/user_entity.dart class UserEntity { // Correct: Uses pure Dart types. final String id; final int profileColorValue; // Storing as an integer is platform-agnostic. } ````❌ Bad Example
````dart // domain/entities/user_entity.dart import 'package:flutter/material.dart';class UserEntity {
final String id;
// VIOLATION: Color is a Flutter type.
final Color profileColor; // <-- LINT WARNING HERE
}
</details>
### `enforce_model_to_entity_mapping`
- **Purpose:** To ensure every **Model** has a defined way to be converted into an **Entity**.
- **Description:** This lint checks every class that matches the `Model` naming convention and verifies that it has a `toEntity()` method. This guarantees a consistent mapping pattern across the entire data layer.
<details>
<summary>✅ Good Example</summary>
````dart
// data/models/user_model.dart
class UserModel extends UserEntity {
// ... fields ...
// Correct: The method exists.
UserEntity toEntity() => UserEntity(id: id, name: name);
}
❌ Bad Example
// data/models/user_model.dart
// VIOLATION: The `toEntity()` method is missing.
class UserModel extends UserEntity { // <-- LINT WARNING HERE
// ... fields ...
// Missing the `toEntity()` method.
}
enforce_model_inherits_entity #
- Purpose: To guarantee structural compatibility between a Model and its Entity.
- Description: Enforces that a class matching the
Modelnaming convention mustextendorimplementits correspondingEntity. This makes thetoEntity()mapping process safer and more logical.
✅ Good Example
// data/models/user_model.dart
// Correct: Inherits from the entity.
class UserModel extends UserEntity { ... }
❌ Bad Example
// data/models/user_model.dart
// VIOLATION: `UserModel` does not extend or implement `UserEntity`.
class UserModel { ... } // <-- LINT WARNING HERE
enforce_layer_independence #
- Purpose: To enforce the correct dependency flow for the entire application.
- Description: This is the master rule for dependency direction. It checks
importstatements and ensures that Presentation -> Domain and Data -> Domain, but never the other way around.
✅ Good Example
// presentation/bloc/auth_bloc.dart
// Correct: The presentation layer imports from the domain layer.
import 'package:my_app/features/auth/domain/usecases/login_usecase.dart';
❌ Bad Example
// domain/usecases/login_usecase.dart
// VIOLATION: The domain layer cannot import from the data layer.
import 'package:my_app/features/auth/data/models/user_model.dart'; // <-- LINT WARNING HERE
enforce_abstract_data_source_dependency #
- Purpose: To enforce the Dependency Inversion Principle.
- Description: Ensures that your
RepositoryImpldepends on a data source abstraction (e.g.,AuthDataSource) and not a concrete implementation (e.g.,DefaultAuthDataSource).
✅ Good Example
// data/repositories/auth_repository_impl.dart
class AuthRepositoryImpl implements AuthRepository {
// Correct: Depends on the abstraction.
final AuthDataSource _dataSource;
AuthRepositoryImpl(this._dataSource);
}
❌ Bad Example
// data/repositories/auth_repository_impl.dart
class AuthRepositoryImpl implements AuthRepository {
// VIOLATION: Depends on the concrete implementation.
final DefaultAuthDataSource _dataSource;
AuthRepositoryImpl(this._dataSource); // <-- LINT WARNING HERE
}
enforce_file_and_folder_location #
- Purpose: To ensure files are located in the correct directories based on their names.
- Description: This lint checks if a class named, for example,
AuthRepositoryis located in a directory configured for domain repositories (e.g.,.../domain/contracts/).
✅ Good Example
// File is located at: lib/features/auth/domain/contracts/auth_repository.dart
// Correct: Location matches the `layer_definitions` config.
abstract interface class AuthRepository { ... }
❌ Bad Example
// File is located at: lib/features/auth/domain/repositories/auth_repository.dart
// VIOLATION: The configured path is 'contracts', not 'repositories'.
abstract interface class AuthRepository { ... } // <-- LINT WARNING HERE
enforce_naming_conventions #
- Purpose: To ensure all major classes follow a consistent naming format.
- Description: This lint checks the names of classes in different sub-layers (
Entity,Model,UseCase,Repository,DataSource) and verifies they match the templates defined in your configuration (e.g.,{{name}}Repository).
✅ Good Example
// In a repository file:
// Correct: Name matches the `{{name}}Repository` template.
abstract interface class AuthRepository { ... }
❌ Bad Example
// In a repository file:
// VIOLATION: Name does not match the `{{name}}Repository` template.
abstract interface class AuthRepo { ... } // <-- LINT WARNING HERE
enforce_custom_return_type #
- Purpose: To enforce that all asynchronous operations in the domain and data layers return a consistent type.
- Description: This lint checks the return types of methods in
UseCasesandRepositoryinterfaces and ensures they match the configured type, which is typically aResultorEithertype like yourFutureEither.
✅ Good Example
// In a repository file:
abstract interface class AuthRepository {
// Correct: Returns the configured `FutureEither` type.
FutureEither<UserEntity> getUser(String id);
}
❌ Bad Example
// In a repository file:
abstract interface class AuthRepository {
// VIOLATION: Returns a raw Future, not `FutureEither`.
Future<UserEntity> getUser(String id); // <-- LINT WARNING HERE
}
enforce_use_case_inheritance #
- Purpose: To ensure all use case classes adhere to a standard contract.
- Description: This lint verifies that any class identified as a
UseCaseextends or implements one of the base classes fromclean_architecture_core(UnaryUseCaseorNullaryUseCase).
✅ Good Example
// In a use case file:
// Correct: Implements the base `UnaryUseCase`.
class GetUserUsecase implements UnaryUseCase<UserEntity, String> { ... }
❌ Bad Example
// In a use case file:
// VIOLATION: Is a plain class, does not implement a base use case.
class GetUserUsecase { ... } // <-- LINT WARNING HERE
enforce_repository_inheritance #
- Purpose: To ensure all repository interfaces adhere to a standard contract.
- Description: This lint verifies that any abstract class identified as a domain
Repositoryextends or implements the baseRepositoryclass fromclean_architecture_core.
✅ Good Example
// In a repository file:
// Correct: Implements the base `Repository`.
abstract interface class AuthRepository implements Repository { ... }
❌ Bad Example
// In a repository file:
// VIOLATION: Is a plain interface, does not implement the base `Repository`.
abstract interface class AuthRepository { ... } // <-- LINT WARNING HERE
missing_use_case #
- Purpose: To identify business logic in a repository that does not have a corresponding
UseCase. - Description: This is an "assistant" lint. It scans the methods of a
Repositoryinterface and checks if a correspondingUseCasefile exists. If not, it provides a warning and a Quick Fix to generate the boilerplateUseCaseclass automatically.
✅ Good Example
// In a repository file:
// (No warning appears because `lib/.../usecases/get_user_usecase.dart` exists)
abstract interface class AuthRepository implements Repository {
FutureEither<UserEntity> getUser(String id);
}
❌ Bad Example
// In a repository file:
abstract interface class AuthRepository implements Repository {
// VIOLATION: No file exists at `lib/.../usecases/login_usecase.dart`.
FutureEither<void> login(String email, String password); // <-- LINT WARNING HERE
}
Full Configuration #
For a complete, well-documented configuration file with all available options, please refer to the analysis_options.yaml in our example project.