← Back to posts Cover image for Mastering Custom Linting in Flutter: Enforcing Code Standards and Automating Best Practices

Mastering Custom Linting in Flutter: Enforcing Code Standards and Automating Best Practices

· 5 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

Unlock Your Team’s Code Quality with Custom Linter Rules

We’ve all been there: reviewing a pull request and spotting a build method that scrolls for days, or finding yet another data class missing a copyWith method. Team conventions drift over time, and while the Dart analyzer’s built-in lints are excellent, they can’t know your project’s specific pain points. What if you could teach the analyzer your team’s rules?

The solution lies in creating custom lint rules using Dart’s analyzer plugin system. This isn’t just about adding warnings; it’s about automating consistency and baking your team’s hard-won lessons directly into the development workflow.

Why Bother with Custom Lints?

Imagine preventing these issues before they reach code review:

  • Widget Bloat: Enforcing a maximum line count for build methods.
  • Data Class Compliance: Ensuring all model classes implement ==, hashCode, and serialization methods.
  • Pattern Enforcement: Banning certain anti-patterns (like using dynamic in specific places) or requiring factories for certain types.

The payoff is less time spent on nitpicking reviews and more consistent, maintainable code across your entire project.

Setting Up Your Custom Lint Package

First, create a new Dart package. This won’t be a Flutter app or a plugin used in your UI—it’s a standalone analyzer plugin.

# pubspec.yaml for your custom_lints package
name: my_team_lints
version: 1.0.0
environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  analyzer: ^6.0.0
  analyzer_plugin: ^0.11.0

The core of your plugin is a class that extends ServerPlugin from the analyzer_plugin package. This class acts as the entry point, telling the analyzer what your plugin provides.

// lib/my_team_lints_plugin.dart
import 'package:analyzer_plugin/plugin/plugin.dart';
import 'package:analyzer_plugin/protocol/protocol_generated.dart';

class MyTeamLintsPlugin extends ServerPlugin {
  MyTeamLintsPlugin() : super(resourceProvider: resourceProvider);

  @override
  List<DiagnosticContributor> getDiagnosticContributors(String s) {
    return [MyLintContributor()];
  }

  @override
  List<CodeCompletionContributor> getCompletionContributors(String s) {
    return [];
  }
}

class MyLintContributor extends DiagnosticContributor {
  @override
  Future<void> computeDiagnostics(
      AnalysisDiagnosticContext context,
      void Function(DiagnosticMessage) callback) async {
    // Your linting logic will go here
  }
}

Writing Your First Rule: The Bloated Build Method

Let’s tackle a common issue: overly long build methods. We’ll create a rule that flags any build method exceeding 30 lines.

The key is working with the AST (Abstract Syntax Tree). The analyzer parses your code into a tree structure, and your plugin traverses it, looking for specific patterns.

// lib/rules/long_build_rule.dart
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/source/source_range.dart';

class LongBuildRule extends GeneralizingAstVisitor<void> {
  final List<AnalysisError> errors = [];

  @override
  void visitMethodDeclaration(MethodDeclaration node) {
    // Check if this is a method named 'build'
    if (node.name.lexeme == 'build') {
      // Check if its parent is a class that extends/with/mixes a Widget
      final parent = node.parent;
      if (parent is ClassDeclaration) {
        // Simple check: count the lines in the method body
        final sourceRange = node.body.sourceRange;
        final lineCount = _countLinesInRange(sourceRange.offset, sourceRange.end);

        // Define our threshold
        if (lineCount > 30) {
          final error = AnalysisError(
            node.source,
            node.offset,
            node.length,
            AnalysisErrorType(
              'CUSTOM_LINT',
              'LongBuildMethod',
              'Build method is too long ($lineCount lines). Consider refactoring into smaller widgets.',
            ),
            severity: ErrorSeverity.WARNING,
          );
          errors.add(error);
        }
      }
    }
    super.visitMethodDeclaration(node);
  }

  int _countLinesInRange(int start, int end) {
    // Implementation would calculate lines from the source content
    // This is a simplified placeholder
    return (end - start) ~/ 20; // Approximation for example
  }
}

To use this rule, you’d instantiate the visitor in your MyLintContributor and run it over the AST provided by the context.

Integrating the Lint into Your Flutter Project

  1. Publish or Path Reference: You can publish your my_team_lints package, or use a local path reference.
  2. Add to analysis_options.yaml: In your Flutter project, point the analyzer to your plugin.
# analysis_options.yaml in your Flutter app
analyzer:
  plugins:
    - my_team_lints
  1. Add Dependency: Add your lints package to the dev_dependencies of the Flutter app’s pubspec.yaml.

Once configured, violations will appear directly in your IDE alongside other analyzer warnings and errors.

Common Pitfalls and Tips

  • AST Exploration is Key: The hardest part is often figuring out how to identify the code pattern you want to lint. Use the ast_visitor package or write small Dart scripts that print AST nodes to understand the structure.
  • Performance Matters: Your visitor runs on every file change. Keep logic efficient and avoid expensive operations.
  • Start Small: Begin with one or two high-impact rules. A rule that flags missing copyWith in @immutable classes is often a great first win.
  • Clear Error Messages: The error or warning message is your user interface. Make it actionable. Instead of “Bad method,” write “The build method exceeds 30 lines. Extract the ListView section into a _buildItemList widget.”

Beyond Warnings: Quick Fixes

The true power emerges when you pair a diagnostic with a quick fix. The plugin system allows you to suggest edits—like generating a missing copyWith method or extracting a widget. This transforms the linter from a nagging critic into an active coding assistant.

By investing a little time in custom lint rules, you codify your team’s standards. You reduce cognitive load for developers and free up code reviews for architectural discussion, not style debates. Start with one rule that addresses your most frequent code review comment, and watch your codebase—and your team’s productivity—improve.

This blog is produced with the assistance of AI by a human editor. Learn more

Related Posts

Cover image for Flutter App Size Optimization: Strategies to Shrink Your APK/IPA Below 10MB

Flutter App Size Optimization: Strategies to Shrink Your APK/IPA Below 10MB

Flutter apps are often criticized for their larger-than-native binary sizes. This post will dive into practical, actionable strategies to significantly reduce your Flutter app's footprint, including build configurations, asset optimization, and dependency management, demonstrating how to achieve app sizes under 10MB.

Cover image for Mastering In-App Subscriptions in Flutter: A Guide to Avoiding Common Pitfalls

Mastering In-App Subscriptions in Flutter: A Guide to Avoiding Common Pitfalls

Implementing in-app subscriptions in Flutter can be surprisingly complex, especially when dealing with cross-platform nuances, receipt validation, and managing different store configurations. This post will delve into the common challenges developers face, compare solutions like RevenueCat with custom implementations, and provide best practices for setting up and maintaining a robust subscription system.

Cover image for Taming Android Emulator RAM: A Developer's Guide to Reducing Memory Usage for Flutter

Taming Android Emulator RAM: A Developer's Guide to Reducing Memory Usage for Flutter

Android emulators are notorious for hogging RAM, often pushing developer machines into swap and slowing down Flutter development. This post will explore the reasons behind high emulator memory consumption and provide practical, actionable strategies and tools (including a custom-built solution) to significantly reduce RAM usage, ensuring a smoother development experience without sacrificing functionality.