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
Flutter for High-Performance Desktop: Is it Ready for CAD, Image Processing, and Complex GUIs?
Developers are curious about Flutter's capabilities beyond typical business apps, especially for demanding desktop applications like CAD/CAM or image/video processing. This post will explore Flutter's suitability for high-performance, viewport-based desktop GUIs, discussing Dart's memory model, the 60fps update loop, and real-world examples to gauge its readiness for 'serious' complex software.
Debugging Flutter Web Navigation: Solving the Deep Link Refresh Bug
Flutter web applications often suffer from a frustrating 'deep link refresh bug' where refreshing the browser on a nested route (e.g., /home/details) bounces the user back to the root or an incorrect path. This post will diagnose the common causes of this issue, explain how Flutter's router handles web URLs, and provide practical solutions and best practices for building robust, refresh-proof navigation in your Flutter web apps.
Mastering Internationalization in Flutter: Centralized Strings for Scalable Apps
As Flutter applications grow, managing strings for multiple languages or just keeping text consistent becomes a challenge. This post will guide developers through effective strategies for centralizing strings, implementing robust internationalization (i18n) and localization (l10n), and leveraging tools to streamline the process for small to large-scale projects.