Elevating Flutter Code Quality with AI: Custom Lints for Consistent, Bot-Proof Development
The Flutter news you actually need
No spam, ever. Unsubscribe in one click.
Flutter development is always evolving, and with AI coding assistants, our workflows are changing faster than ever. These tools are fantastic for boosting productivity, but they introduce a new challenge: how do we ensure the AI generates code that consistently adheres to our project’s specific conventions and quality standards?
You might find yourself creating extensive AGENTS.md files or detailed prompts, painstakingly listing “do’s and don’ts” for your AI assistant. “Always use const where possible,” “Prefix all data models with App,” “Never use setState in this module.” This approach quickly becomes cumbersome. The AI burns valuable context on these rules, and you still end up reviewing code for violations that could have been prevented automatically.
The Problem with Context-Heavy AI Directives
Think about it: every time your AI assistant processes a request, it has to parse and understand a potentially long list of custom rules. This consumes precious context window, slows down responses, and isn’t foolproof. The AI might misinterpret a nuance or simply miss a rule in a complex scenario. For human developers, it’s even worse – relying on memory or constantly checking a separate document is inefficient and error-prone.
What if we could codify these rules directly into our development pipeline? What if our IDE could instantly flag convention violations, not just for humans, but also for the AI that generates the code?
The Solution: Custom Lint Rules
This is where custom lint rules shine. By creating your own set of lint rules, you can embed your project’s unique coding standards directly into the Dart analyzer. This means that both human developers and AI agents are guided by the same automated checks. If the AI generates code that breaks a rule, your IDE will immediately highlight it, prompting the AI (or you) to correct it before it even reaches a pull request.
Let’s walk through how to create a simple custom lint rule.
Step 1: Create Your Custom Lint Package
First, we need a separate Dart package to house our custom lint rules. Let’s call it my_project_linter.
mkdir my_project_linter
cd my_project_linter
dart create -t package-lib .
Now, open pubspec.yaml in my_project_linter and add the custom_lint dependency:
# my_project_linter/pubspec.yaml
name: my_project_linter
description: Custom lint rules for My Project.
version: 0.0.1
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
analyzer: ^6.0.0 # Use the analyzer version compatible with your SDK
custom_lint_core: ^0.5.0 # Keep this updated
dev_dependencies:
lints: ^2.0.0
test: ^1.21.0
custom_lint: ^0.6.0 # For running/testing your lints
Step 2: Define Your Custom Lint Rule
Let’s create a rule that enforces a specific convention: all service classes (classes ending with Service) must be prefixed with App. So, UserService should be AppUserService, AuthService should be AppAuthService, and so on.
Create a file lib/src/app_service_prefix_lint.dart in your my_project_linter package:
// my_project_linter/lib/src/app_service_prefix_lint.dart
import 'package:analyzer/custom_lint/custom_lint.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
class AppServicePrefixLint extends CustomLint {
@override
void run(CustomLintConfigs configs, ResultReporter reporter) {
reporter.atCompilationUnit.listen((unit) {
unit.visitChildren(
_AppServicePrefixVisitor(
reporter: reporter,
unit: unit,
),
);
});
}
}
class _AppServicePrefixVisitor extends RecursiveAstVisitor<void> {
final ResultReporter reporter;
final CompilationUnit unit;
_AppServicePrefixVisitor({required this.reporter, required this.unit});
@override
void visitClassDeclaration(ClassDeclaration node) {
final className = node.name.lexeme;
if (className.endsWith('Service') && !className.startsWith('App')) {
reporter.reportErrorForNode(
AppServicePrefixCode.code,
node.name, // Report error on the class name
);
}
super.visitClassDeclaration(node);
}
}
class AppServicePrefixCode extends LintCode {
static const code = AppServicePrefixCode(
name: 'app_service_prefix',
problemMessage: 'Service classes must be prefixed with "App". '
'Consider renaming "{0}" to "App{0}".',
correctionMessage: 'Add "App" prefix to the service class name.',
url: 'https://your-project.dev/docs/lints#app_service_prefix', // Optional: link to your internal docs
);
const AppServicePrefixCode({
required super.name,
required super.problemMessage,
super.correctionMessage,
super.url,
});
}
Now, export your lint rule from my_project_linter/lib/my_project_linter.dart:
// my_project_linter/lib/my_project_linter.dart
import 'package:analyzer/custom_lint/custom_lint.dart';
import 'src/app_service_prefix_lint.dart';
// This is the entry point of your custom_lint package.
// It must have the name `createPlugin`.
PluginBase createPlugin() => _MyProjectLinter();
class _MyProjectLinter extends PluginBase {
@override
List<Lint> getLints(CustomLintConfigs configs) => [
AppServicePrefixLint(),
];
}
Step 3: Integrate into Your Flutter Project
Now, in your main Flutter project (e.g., my_flutter_app), add my_project_linter as a dev_dependency:
# my_flutter_app/pubspec.yaml
name: my_flutter_app
description: A new Flutter project.
publish_to: 'none'
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
# ... other dependencies
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
custom_lint: ^0.6.0 # Required to run custom lints
my_project_linter:
path: ../my_project_linter # Path to your custom lint package
Finally, configure your analysis_options.yaml to enable custom_lint:
# my_flutter_app/analysis_options.yaml
include: package:flutter_lints/flutter.yaml
analyzer:
plugins:
- custom_lint # Enable custom_lint plugin
linter:
rules:
# Your regular lint rules here
- prefer_const_constructors
- avoid_print
# You can also configure your custom lints if they support it
# custom_lint:
# rules:
# app_service_prefix:
# enabled: true
# # severity: warning # Example of custom severity
See It in Action
Now, if you create a service class in your Flutter project without the App prefix:
// my_flutter_app/lib/services/user_service.dart
class UserService { // <-- This will be flagged by your custom lint!
String getUserName() => 'John Doe';
}
// Corrected version:
class AppUserService {
String getUserName() => 'John Doe';
}
Your IDE (VS Code, IntelliJ) will immediately show a warning or error, just like any built-in Dart lint. The analyzer now understands and enforces your specific project convention.
The Benefits
- Automated Consistency: No more manual reviews for common stylistic or architectural issues. The analyzer catches them instantly.
- Reduced AI Context: Your AI prompts can now focus on the what to build, not the how to format or name things. The linter handles the conventions.
- Faster Development Cycles: Fewer back-and-forth corrections, quicker code integration.
- Onboarding Made Easy: New team members (human or AI) quickly learn and adopt project standards without extensive documentation reading.
- Higher Code Quality: By enforcing best practices consistently, your codebase becomes more readable, maintainable, and less prone to errors.
Integrating custom lint rules is a powerful step towards a more efficient and consistent development workflow. It’s about letting the tools handle repetitive checks, freeing you and your AI assistants to focus on solving complex problems and building amazing Flutter experiences. Give it a try; your future self (and your AI) will thank you!
This blog is produced with the assistance of AI by a human editor. Learn more
Related Posts
Optimizing Flutter UI Performance: Best Practices for Date Formatting and Expensive Operations
Developers often face performance bottlenecks when performing expensive operations like date formatting directly within Flutter's `build` method, especially in fast-scrolling lists. This post will delve into common pitfalls, explain why these operations are costly, and provide practical strategies for optimizing UI performance by caching formatters, using `initState`, and leveraging `compute` for background processing without blocking the UI.
Optimizing Your Flutter Dev Setup: IDEs, Simulators, and AI Tools for Peak Productivity
Flutter developers frequently seek to refine their development environments. This post will dive into popular IDE choices like VS Code and Android Studio, discuss best practices for managing iOS and Android simulators (including in-IDE options), and explore the practical integration of AI tools for code generation and problem-solving to boost overall efficiency.
Demystifying Flutter Performance: Practical Strategies for Large-Scale Apps
Flutter's performance is often blamed for issues in complex applications, but the real culprits are usually architectural decisions, inefficient widget rebuilds, and unoptimized resource handling. This post will dive into common performance bottlenecks in large Flutter apps, providing actionable strategies for profiling, optimizing state management, handling images and network requests efficiently, and leveraging CI/CD for continuous performance monitoring.