年末大掃除：`flutter_gen`で使っている未使用の画像リソースを抽出する

この記事はand factoryの2024年アドベントカレンダーの記事です。

https://qiita.com/advent-calendar/2024/andfactory


Flutterでアプリ開発している渡邉です。


年末も近づいてきて、Flutterのプロジェクトも大掃除（リファクタリング）したいところです。
この度、DevToolsでアプリのリソースを確認したところアプリ内の画像リソースが多くを占めていることが分かりましたので、どのリソースが実質使われていないのかを特定してみました。

DevToolsではAppSizeタブがありサイズ情報を確認することができます。詳しくは公式のドキュメントをご参照ください。

![](https://docs.flutter.dev/assets/images/docs/tools/devtools/app_size_analysis.png)

https://docs.flutter.dev/tools/devtools/app-size


開発中のプロジェクトでは、以下のような理由でアプリ内の不要な画像リソースが残っていることがあります。
- 古いUIで使用されていた画像が未削除
- 機能のリファクタリング後に使用されなくなった画像
- テスト用リソースが残存

今回は、Flutterプロジェクトで `flutter_gen` を使って生成された画像リソースの中から、**未使用の画像リソースを効率的に抽出する方法**を紹介します。

---

## `flutter_gen` とは？

[`flutter_gen`](https://pub.dev/packages/flutter_gen) は、Flutterプロジェクトで定義されたアセット（画像やフォントなど）を安全に利用するための自動生成ツールです。`pubspec.yaml` に記述されたアセット情報から、型安全なDartコードを生成します。

例えば、以下のようなコードを生成します：

```dart
/// File path: assets/images/sample_image.webp
AssetGenImage get sampleImage =>
    const AssetGenImage('assets/images/sample_image.webp');
```

これにより、手動でファイルパスを記述する手間を省き、コードの保守性を高めます。

---

## 未使用リソースを抽出するスクリプト

以下は、`flutter_gen` が生成したコードを解析し、未使用の画像リソースを抽出するDartスクリプトです。

このスクリプトは以下の手順で動作します：
1. `flutter_gen` が生成したファイル（通常は `lib/gen/assets.gen.dart`）を解析して、全てのアセット情報を取得
2. プロジェクト全体をスキャンし、各アセットがどのファイルで使用されているか記録する
3. 未使用のアセットをリストアップ

コードが汚かったのでChatGPTに整形していただきました。

```dart
import 'dart:io';

void main() {
  // Generatedアセットファイルを指定
  final assetsFile = File('lib/gen/assets.gen.dart');

  // ファイルが存在しない場合のエラーチェック
  if (!assetsFile.existsSync()) {
    print('assets.gen.dart が見つかりません。パスを確認してください。');
    return;
  }

  // assets.gen.dart 内のゲッターメソッド名とアセットパスを取得
  final getterNames = <String>[]; // 生成されるゲッター名
  final assetPaths = <String, String>{}; // {getterName: filePath}

  final lines = assetsFile.readAsLinesSync();
  for (var i = 0; i < lines.length; i++) {
    // ファイルパスを取得
    final filePathMatch =
        RegExp(r'/// File path: (.+)').firstMatch(lines[i]);
    if (filePathMatch != null && i + 1 < lines.length) {
      final filePath = filePathMatch.group(1)!;

      // 次の行でゲッターメソッドを検索
      final getterMatch =
          RegExp(r'AssetGenImage get ([a-zA-Z0-9_]+) =>').firstMatch(lines[i + 1]);
      if (getterMatch != null) {
        final getterName = getterMatch.group(1)!;
        getterNames.add(getterName);
        assetPaths[getterName] = filePath;
      }
    }
  }

  if (getterNames.isEmpty) {
    print('アセットが見つかりませんでした。');
    return;
  }

  // プロジェクト全体で使用されているアセットゲッターを検索
  final projectFiles = Directory('./lib').listSync(recursive: true);
  final usageDetails = <String, List<String>>{}; // {getterName: [filePaths]}

  for (var getter in getterNames) {
    usageDetails[getter] = []; // 初期化
  }

  for (var file in projectFiles) {
    if (file is File && file.path.endsWith('.dart')) {
      // ./lib/gen/assets.gen.dart を除外
      if (file.path.contains('lib/gen/assets.gen.dart')) {
        continue;
      }

      final content = file.readAsLinesSync();
      for (var line in content) {
        // コメント行をスキップ
        if (line.trim().startsWith('//')) {
          continue;
        }

        for (var getter in getterNames) {
          if (line.contains(getter)) {
            usageDetails[getter]!.add(file.path);
          }
        }
      }
    }
  }

  // 使用状況をログに出力
  print('\nアセット使用状況 (使用回数順):');
  final sortedUsageDetails = usageDetails.entries.toList()
    ..sort((a, b) => b.value.length.compareTo(a.value.length)); // 使用回数でソート

  for (var entry in sortedUsageDetails) {
    final getter = entry.key;
    final usageCount = entry.value.length;
    print('${getter} (${assetPaths[getter]}): $usageCount回使用');
    if (usageCount > 0) {
      print('  使用ファイル:');
      for (var path in entry.value) {
        print('    - $path');
      }
    }
  }

  // 未使用アセットを特定
  final unusedAssets = sortedUsageDetails.where((entry) => entry.value.isEmpty);

  if (unusedAssets.isEmpty) {
    print('\nすべてのアセットが使用されています。');
  } else {
    print('\n未使用のアセット:');
    for (var entry in unusedAssets) {
      print('${entry.key}: ${assetPaths[entry.key]}');
    }
  }
}
```

---

### **スクリプトの実行方法**

1. プロジェクトのルートディレクトリにスクリプトを保存（例: `check_unused_assets.dart`）
2. 以下のコマンドで実行します

```bash
dart check_unused_assets.dart
```

---

## 出力例

以下はスクリプトを実行した際のサンプル出力です。

### **アセット使用状況**
```plaintext
アセット使用状況 (使用回数順):
imgSample (assets/gifs/img_sample.webp): 3回使用
  使用ファイル:
    - lib/screens/home_screen.dart
    - lib/screens/my_page_screen.dart
    - lib/widgets/header.dart
anotherImage (assets/images/another_image.webp): 0回使用
```

### **未使用アセット**
```plaintext
未使用のアセット:
anotherImage: assets/images/another_image.webp
```

VSCodeのターミナルからCmd+クリックでファイルに飛べますのでサクサク作業できます。


## まとめ

このスクリプトを使うことで、`flutter_gen` で管理されている画像リソースの中から未使用のものを効率的に見つけることができます。未使用リソースを削除することで、アプリのビルドサイズを削減し、パフォーマンスの向上が期待できます。

年末の大掃除の一環として、ぜひ活用してみてください