Linting and analysis_options in Flutter: solved exercise

Linting and analysis_options in Flutter: solved exercise

Dart’s static analyzer catches errors, bad practices, and dead code before you run the app. A well-configured analysis_options.yaml is the first quality gate in any professional project.

Problem statement

Configure analysis_options.yaml with:

  • flutter_lints rules as the base.
  • Additional production rules (prefer_final, avoid_print, etc.).
  • Analysis warnings treated as compile errors.
  • Exclusions for generated code.

Then fix the most common warnings in real Dart code.

Dependencies

1
2
3
4
dev_dependencies:
  flutter_lints: ^5.0.0
  # Stricter alternative (optional):
  # very_good_analysis: ^7.0.0

Solution: complete analysis_options.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# analysis_options.yaml (at project root)
include: package:flutter_lints/flutter.yaml

analyzer:
  # Treat warnings as errors (recommended in CI)
  errors:
    invalid_annotation_target: ignore   # required with Freezed
    missing_required_param: error
    dead_code: warning
    unused_import: error
    unused_local_variable: warning

  exclude:
    - "**/*.g.dart"           # generated by build_runner / json_serializable
    - "**/*.freezed.dart"     # generated by Freezed
    - "lib/generated/**"      # generated i18n
    - "test/.test_coverage.dart"

linter:
  rules:
    # ── Code quality ──────────────────────────────────────────────────────
    prefer_final_locals: true          # var → final when not reassigned
    prefer_final_fields: true          # immutable fields → final
    prefer_const_constructors: true    # use const wherever possible
    prefer_const_literals_to_create_immutables: true
    unnecessary_const: true
    unnecessary_new: true

    # ── Safety and bugs ───────────────────────────────────────────────────
    avoid_print: true                  # use debugPrint or a logger
    avoid_dynamic_calls: true          # no .call() on dynamic
    always_use_package_imports: false  # relative imports OK within same package
    cancel_subscriptions: true         # StreamSubscription.cancel()
    close_sinks: true                  # Sink.close()
    unawaited_futures: true            # await all relevant Futures

    # ── Style ─────────────────────────────────────────────────────────────
    directives_ordering: true          # sorted imports
    sort_pub_dependencies: false       # pub.dev sorts them automatically
    lines_longer_than_80_chars: false  # dart fmt handles this
    flutter_style_todos: true          # TODO(name): description

    # ── Nullability ───────────────────────────────────────────────────────
    prefer_null_aware_operators: true
    avoid_null_checks_in_equality_operators: true
    null_closures: true

    # ── Performance ───────────────────────────────────────────────────────
    avoid_unnecessary_containers: true  # no unnecessary Container
    use_colored_box: true               # Container(color:) → ColoredBox
    use_decorated_box: true             # Container(decoration:) → DecoratedBox

Examples: code with warning and its fix

1. avoid_print

1
2
3
4
5
6
7
8
9
// ❌ Warning: avoid_print
void fetchData() {
  print('Loading data...');
}

// ✅ Correct
void fetchData() {
  debugPrint('Loading data...'); // only in debug mode
}

2. prefer_final_locals

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// ❌ Warning: prefer_final_locals
void processUser(Map<String, dynamic> json) {
  var name = json['name'] as String;   // name is never reassigned
  var age = json['age'] as int;
  debugPrint('$name, $age');
}

// ✅ Correct
void processUser(Map<String, dynamic> json) {
  final name = json['name'] as String;
  final age = json['age'] as int;
  debugPrint('$name, $age');
}

3. use_colored_box

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// ❌ Warning: use_colored_box
Widget build(BuildContext context) {
  return Container(
    color: Colors.red,
    child: const Text('Hello'),
  );
}

// ✅ Correct (more efficient, no extra layout layer)
Widget build(BuildContext context) {
  return const ColoredBox(
    color: Colors.red,
    child: Text('Hello'),
  );
}

4. unawaited_futures

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// ❌ Warning: unawaited_futures
void onButtonTap() {
  saveToDatabase();   // unawaited Future
}

// ✅ Correct
Future<void> onButtonTap() async {
  await saveToDatabase();
}

// ✅ Alternative if intentional:
void onButtonTap() {
  unawaited(saveToDatabase()); // import 'dart:async'
}

5. cancel_subscriptions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// ❌ Warning: cancel_subscriptions
class _MyWidgetState extends State<MyWidget> {
  late StreamSubscription<int> _sub;

  @override
  void initState() {
    super.initState();
    _sub = stream.listen((_) {});
  }
  // dispose does not cancel _sub → memory leak
}

// ✅ Correct
@override
void dispose() {
  _sub.cancel();
  super.dispose();
}

Running the analyzer from the terminal

1
2
3
4
5
6
7
8
# Full project analysis
dart analyze

# Errors only (ignores warnings)
dart analyze --fatal-warnings

# Auto-fix simple issues
dart fix --apply

CI/CD integration

1
2
3
4
5
6
# .github/workflows/ci.yml (excerpt)
- name: Analyze
  run: dart analyze --fatal-warnings

- name: Format check
  run: dart format --set-exit-if-changed .

very_good_analysis: when to use it

very_good_analysis is stricter than flutter_lints and enables ~200 rules. It is the option used by Very Good Ventures in production projects. If you are starting a new project with high standards, start with it:

1
2
3
4
5
6
# pubspec.yaml
dev_dependencies:
  very_good_analysis: ^7.0.0

# analysis_options.yaml
include: package:very_good_analysis/analysis_options.yaml

Common mistakes

  • Silencing all warnings with // ignore:: inline ignores should be the exception, not the rule. If a warning appears in many places, evaluate whether the rule suits your project and adjust analysis_options.yaml.
  • Not excluding generated files: *.g.dart and *.freezed.dart generate hundreds of false warnings if not excluded.
  • Adding --fatal-warnings in CI without cleaning warnings first: the pipeline fails from the first commit. Fix all warnings before enabling it.

Practical use

A project with strict linting from day 1 catches bugs before they reach review, standardizes style without PR debates, and makes onboarding faster because the code is predictable.

Guided practice and next step

FAQ

Does the linter slow down compilation?

No. Analysis runs statically and in parallel with the compiler. It does not affect flutter run or flutter build time.

Can I disable a rule for a single file?

Yes. Add at the top of the file:

1
// ignore_for_file: avoid_print

Or for a single line:

1
print('debug'); // ignore: avoid_print

Is dart fix --apply safe?

For most automatic fixes, yes — they are tested code equivalences. Always review the diff before committing, especially on large projects.