← Back to posts Cover image for Boosting Flutter Code Quality: Finding & Removing Dead Code 'dart analyze' Misses

Boosting Flutter Code Quality: Finding & Removing Dead Code 'dart analyze' Misses

· 5 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

As Flutter projects grow, they inevitably accumulate technical debt. One of the most common and insidious forms is dead code: functions no one calls, widgets that are never rendered, or entire classes that have been orphaned by refactoring. While dart analyze is our trusty first line of defense, it’s not infallible. It can miss a surprising amount of unused code, especially in dynamic frameworks like Flutter. This hidden bloat slows down builds, increases app size, and makes the codebase harder to understand and maintain.

Let’s explore why the standard analyzer falls short and look at practical strategies and tools to thoroughly clean house.

Why dart analyze Misses Certain Dead Code

dart analyze is fantastic for catching statically determinable issues. It will flag a private function in a file if it’s never used within that same file. However, its scope has intentional limitations:

  1. Cross-File Analysis Gaps: A public function calculateDiscount() defined in utils/pricing.dart might no longer be imported or called anywhere in your project, but dart analyze won’t flag it. Its job is to analyze individual files and their direct dependencies, not perform a full project-wide usage graph.
  2. Dynamic Invocation: Code that is only referenced via dart:mirrors (rare in Flutter) or through dynamic method calls (e.g., Function.apply, Symbol) can be invisible to the static analyzer.
  3. Generated Code & Conditional Imports: Code paths for other platforms (web vs. mobile) or code generated by builders might appear “unused” in a specific analysis context but are essential for the complete project.

Here’s a classic example dart analyze won’t complain about:

lib/services/old_api_service.dart:

// This service was replaced by 'new_api_service.dart' months ago.
// It's still a public class, so analyzer doesn't flag it in this file.

class OldApiService {
  Future<void> fetchLegacyData() async {
    // ... obsolete network calls ...
  }

  // This utility function is also public and dead.
  static String formatOldData(Map<String, dynamic> data) {
    return 'Old: ${data['id']}';
  }
}

lib/ui/some_screen.dart:

import './services/new_api_service.dart'; // Correct service imported
// Notice: No import for 'old_api_service.dart'

class SomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Only NewApiService is used.
    return FutureBuilder(
      future: NewApiService().fetchData(),
      builder: (context, snapshot) => Text('Data loaded'),
    );
  }
}

The OldApiService is a zombie—compiled into your app but doing nothing. Finding these requires looking at the project as a whole.

Strategies for a Comprehensive Cleanup

1. Leveraging dart fix for Safe Removal

Before reaching for advanced tools, run dart fix --dry-run to see what the Dart tooling can automatically clean up (like unused imports). Then apply it with dart fix --apply. This is safe and effective for the issues it identifies.

2. Introducing Project-Wide Dead Code Detection

This is where community-built tools shine. The core concept is to build a dependency graph of your entire project and then flag any exported declarations (like public classes, top-level functions, public methods) that have no inbound references from your lib/ directory or your entrypoints (main.dart, other .dart files outside lib).

While I won’t link to specific tools, the workflow for using such a tool typically looks like this in your terminal:

# 1. Install the dedicated dead code detection package
# flutter pub global activate your_dead_code_detector

# 2. Run it on your project, often generating a report
# detect_unused --project ./lib --report unused_report.json

# 3. Review the report. It might list files like:
# - lib/services/old_api_service.dart
# - lib/utils/string_formatters.dart (only one unused function inside)

# 4. After creating a backup, proceed with manual deletion.

Best Practices for a Lean Codebase

  1. Regular Audits: Schedule dead code cleanup as part of your sprint grooming or before major releases. Don’t let it build up for years.
  2. Prefer private (_) Visibility: Make classes, functions, and variables private by default. This gives dart analyze more power to warn you within the file. Expose APIs publicly only when necessary.
  3. Use Feature Flags Wisely: If you comment out code “just in case,” use proper feature flags or version control (your git history) instead. Dead commented-out code is still dead weight.
  4. Validate Before Deletion: Always check the tool’s report. Ensure the code isn’t used via dynamic patterns or by a code generator you may have missed. Run your full test suite after removal.

A Quick Script for the DIY-Inclined

For a quick, one-off project scan, you can write a simple script using the Dart analyzer APIs. This example gives you a conceptual starting point:

find_unused.dart (basic conceptual example):

import 'dart:io';
import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/ast/ast.dart';

void main(List<String> args) {
  final projectDir = Directory('./lib');
  final allDeclarations = <String, Set<String>>{};

  // Recursively parse all Dart files in lib
  final dartFiles = projectDir
      .listSync(recursive: true)
      .whereType<File>()
      .where((f) => f.path.endsWith('.dart'));

  for (final file in dartFiles) {
    final content = file.readAsStringSync();
    final unit = parseString(content: content).unit;

    // Visit nodes to collect top-level class and function declarations
    unit.visitChildren(DeclarationCollector(allDeclarations, file.path));
  }

  // In a real tool, you would now cross-reference all these declarations
  // against every identifier used in the project to find which have zero references.
  print('Collected ${allDeclarations.length} top-level declarations.');
  // ... Cross-reference logic would go here ...
}

// A simple visitor to collect declarations (simplified for illustration)
class DeclarationCollector extends RecursiveAstVisitor<void> {
  final Map<String, Set<String>> _declarations;
  final String _filePath;

  DeclarationCollector(this._declarations, this._filePath);

  @override
  void visitClassDeclaration(ClassDeclaration node) {
    _declarations[node.name.name] = (_declarations[node.name.name] ?? {})..add(_filePath);
    super.visitClassDeclaration(node);
  }

  @override
  void visitFunctionDeclaration(FunctionDeclaration node) {
    _declarations[node.name.name] = (_declarations[node.name.name] ?? {})..add(_filePath);
    super.visitFunctionDeclaration(node);
  }
}

Conclusion

Relying solely on dart analyze for dead code cleanup leaves significant gaps in larger Flutter projects. By understanding these limitations and incorporating project-wide analysis tools into your maintenance routine, you can aggressively combat bloat. The result is a faster, smaller, and more understandable codebase—making every developer’s life easier and your app that much more efficient. Start your next cleanup session by looking beyond the standard analyzer.

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.