Bloc File Structure
NOTE: This guide is a work in progress.
Overview
Let's say we want to enforce all developers on our Flutter app to use a feature-first project structure.
A typical feature-first folder structure for a Flutter app may look something like the following:
├── core
├── features
│ ├── cart
│ │ ├── data
│ │ │ ├── models
│ │ │ ├── repositories
│ │ │ ├── services
│ │ ├── domain
│ │ │ ├── entities
│ │ │ ├── repositories
│ │ ├── presentation
│ │ │ ├── controllers
│ │ │ ├── states
│ │ │ ├── widgets
The above structure is roughly based on this great architecture series by Andrea Bizzotto.
In order to enforce the use of such a structure programmatically, we could implement any of the following cases:
- only permit the declaration of widgets in the appropriate
widgets
folders - prevent json serialization/deserialization from anywhere besides
data
layer - enforce the use of dependency injection between layers
- only permit the declaration of business logic in
controllers
folder (such asBlocBase
orNotifier
classes)
In this tutorial, we will focus on the latter case, specifically by disallowing the creation of BLoC classes in any folder besides the presentation > controller layer.
Learning Objectives
- Using
visitCompilationUnit
andunit
to check if a file is in the correct folder - Performance Considerations when writing LintRules
- Using
TypeChecker
to check if a class declaration extends a particular type
Define Requirements
Determining the logic of a Lint rule can be tricky. For this feature-first BLoC rule, we want to prevent a developer from creating a bloc class outside of presentation > controller, but this can possibly be done in different ways. Additionally, we want to be mindful that the performance of all of our lints could be negatively affected if just one lint rule is written in an inefficient way, so we need to write our lint in an efficient way.
Therefore, the logic of our rule will be:
- Restrict our rule from executing on anything besides files contained in
**/presentation/controller/**
folders - Check if any particular class declaration extends
BlocBase
- OPTIONAL: Check if the particular file imports package:bloc before checking all class declarations for a BlocBase extension
With requirements defined, we can move onto the build phase.
Declaring our Rule
Lets start by creating a rule package bloc_feature_structure
and adding sidecar
and analyzer
to pubspec.yaml
.
dart create bloc_feature_structure
name: bloc_feature_structure
version: 1.0.0
environment:
sdk: ">=2.17.0 <3.0.0"
dependencies:
analyzer: ^4.7.0
sidecar: ^0.1.0-dev.18
dev_dependencies:
sidecar_lints: ^0.1.0-dev.1
Our first course of action is to create our LintRule class and its respective LintCode.
import 'package:analyzer/dart/ast/ast.dart';
import 'package:sidecar/sidecar.dart';
class BlocOutsideControllerLayer extends LintRule {
static const id = 'bloc_outside_controller_layer';
static const package = 'bloc_feature_structure';
static const message = 'Logic should be created in application folders.';
LintCode get code => const LintCode(id, package: package);
}
Just as we did in the previous tutorial, we ensure that the class name is the PascalCase representation of the LintCode.id
that we assign, and we also ensure that the LintCode.id is the snake_case representation of the class name. We also make sure
that the package name is the same as the package name in pubspec.yaml
.
NOTE: These requirements can be referenced in the rule creation checklist
Implementing our Rule Logic
Using visitCompilationUnit
to check a file path
From here we can build out the first of our requirements: to only analyze files outside of the application folder.
If you remember back to our feature-first project structure, we only want our rule to analyze files that are outside of the presentation/controllers
folder, since we should not be creating BLoC classes in any other folder.
In Sidecar, rule execution is scoped to a single file, and we can use the unit
context variable to see exactly what file
is being analyzed. We can use the package glob
to check if the file path matches the particular presentation/controllers
folder we're filtering for.
class BlocOutsideControllerLayer extends LintRule {
void visitCompilationUnit(CompilationUnit node) {
final controllersFolderGlob = Glob('**/features/**/controllers/**');
isControllerFile = controllersFolderGlob.matches(unit.path);
}
late final bool isControllerFile;
}
In this example, we create a isControllerFile
variable that will store the result of our glob check. We can then use this variable
in our visitClassDeclaration
method before proceeding with the rest of our logic.
Type-checking a class declaration
Finally, we create our logic that checks if the class declaration extends BlocBase
and if so, we report the lint.
Unfortunately, we cannot simply import BlocBase
from package:bloc
and use it to type check our class declaration,
as the analyzer does not provide us with a Type
object to compare against.
Instead, we must use the TypeChecker
utility shipped in Sidecar, which gives us a simple way to check if an analyzed type
matches a particular type that we're looking for.
NOTE: If you're interested in learning more about the
TypeChecker
utility, you can read more about it here
class BlocOutsideControllerLayer extends LintRule {
late final bool isControllerFile;
void visitClassDeclaration(ClassDeclaration node) {
// dont perform type analysis on files inside of controller folders
if (isControllerFile) return;
// type check against the type of the declared class
final classType = node.declaredElement2?.thisType;
final blocBase = TypeChecker.fromPackage('BlocBase', package: 'bloc');
if (!blocBase.isAssignableFromType(classType)) return;
reportAstNode(node.name, message: message);
}
}
Performance Considerations
The reason we don't handle the file-check logic directly in visitClassDeclaration
is because we always want to avoid unnecessary computation
in the case that a particular file has multiple class declarations. If we were to instead check the file path in visitClassDeclaration
,
we would be redundantly checking the file path every time, and this could be a performance bottleneck.
When writing a lint rule, we want to make sure that we are only performing the necessary computation to ensure that our lint is as performant as possible. This is necessary when developers expect lints to appear after every file change within fractions of a second in the code editor.
Conclusion
TODO