flutter_genでアセット管理してみた

## はじめに

[TRIAL＆RetailAI Advent Calendar 2024](https://qiita.com/advent-calendar/2024/retail-ai) の 9日目の記事です。

昨日は@Carol_fanさんの[『フロントエンドで自動ユニットテストについて』](https://qiita.com/Carol_fan/items/b293237aef65ee48b940)

という記事でした。

私も今、テストについて勉強をしているため、インプット&アウトプットをして

経験を重ねていきたいと思います！

さて、今回はFlutterでのアセット管理において、flutter_genという便利なパッケージが

ありますので、そのご紹介をしたいと思います！


## 自己紹介

TRIALの基盤チームにてモバイルアプリの開発に従事しています。

興味がある方は、以下をご参照下さい。

Retail AI：
https://www.retail-ai.jp/

TRIAL：
https://www.trial-net.co.jp/

採用ページ：

https://www.recruit-retail-ai.jp/
 
https://hrmos.co/pages/trialgp/jobs?category=2020003768196907008

## 目次

* [アセットの追加](https://qiita.com/fujihara_hideyuki/items/d3288d13cdddc3924455#%E3%82%A2%E3%82%BB%E3%83%83%E3%83%88%E3%81%AE%E8%BF%BD%E5%8A%A0)

* [問題点...](https://qiita.com/fujihara_hideyuki/items/d3288d13cdddc3924455#%E5%95%8F%E9%A1%8C%E7%82%B9)

* [解決策 ~ flutter_gen ~](https://qiita.com/fujihara_hideyuki/items/d3288d13cdddc3924455#%E8%A7%A3%E6%B1%BA%E7%AD%96--flutter_gen-)

* [flutter_genのセットアップ](https://qiita.com/fujihara_hideyuki/items/d3288d13cdddc3924455#flutter_gen%E3%81%AE%E3%82%BB%E3%83%83%E3%83%88%E3%82%A2%E3%83%83%E3%83%97%EF%B8%8F)

* [生成 & 利用](https://qiita.com/fujihara_hideyuki/items/d3288d13cdddc3924455#%E7%94%9F%E6%88%90--%E5%88%A9%E7%94%A8)

* [アセットのパスやファイル名を変えたい場合は...？](https://qiita.com/fujihara_hideyuki/items/d3288d13cdddc3924455#%E3%82%A2%E3%82%BB%E3%83%83%E3%83%88%E3%81%AE%E3%83%91%E3%82%B9%E3%82%84%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%90%8D%E3%82%92%E5%A4%89%E3%81%88%E3%81%9F%E3%81%84%E5%A0%B4%E5%90%88%E3%81%AF)

* [まとめ](https://qiita.com/fujihara_hideyuki/items/d3288d13cdddc3924455#%E3%81%BE%E3%81%A8%E3%82%81)

* [参考](https://qiita.com/fujihara_hideyuki/items/d3288d13cdddc3924455#%E5%8F%82%E8%80%83)

## アセットの追加

本記事の導入として、まずは一般的なアセットの追加方法について記載いたします。

手順は以下の通りです。

### 手順1

アセットを表示するために、まずはアセットを配置するディレクトリを

プロジェクトのルートディレクトリに作成します。

よくある命名は`assets`かと思われますので、

今回はこの`assets`ディレクトリに様々なアセットが配置されることを想定して

`assets/images`といったように`images`ディレクトリを作成して、

この`images`ディレクトリにあるpng形式の画像(`check_mark.png`)を

表示させようと思います。

画像は[iconmonstr](https://iconmonstr.com/)にあるものを利用させていただいています。

### 手順2

次に`pubspec.yaml`にて、`flutter`セクションのサブセクションに`assets`を追加して、

その下にアセットのパスを追加します。

```yaml
flutter:
  assets:
    - assets/images/check_mark.png
```

`pub get`も忘れずに。

これでアセットの準備は完了です。

### 手順3

このpng形式の画像を表示する簡単なサンプルを書いてみました。

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Display PNG Image'),
        ),
        body: Center(
          child: Image.asset('assets/images/check_mark.png'),
        ),
      ),
    );
  }
}
```

結果はこちらです。

![スクリーンショット 2024-11-16 10.26.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3634309/32667ede-c8a7-ca39-6738-b9e46415e3cc.png)

ちゃんと表示されていますね。

## 問題点...

上記のコードの`Image.asset('assets/images/check_mark.png')`

とのようにパスを直接指定することでもアセットを参照することはできるのですが、

例えばこれを、`assets/check_mark.png`や`assets/image/check_mark.png`

といったようにTypoをした場合はアセットを参照できず、エラーになってしまいます...

![スクリーンショット 2024-11-16 10.31.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3634309/bf89c308-f02c-ac4d-5627-243b06ec529a.png)

上記のようなパスを直接記述するやり方ですと

アセットを配置するディレクトリが変わったり、

そのファイルの名前が変わったりした場合、

対応するコードと`pubspec.yaml`の設定を修正しなければならず手間が掛かりますし、

また、コンパイルエラーも発生しないので、

「**実行するまでエラーに気付かない**」、といった問題も起こります...

## 解決策🔍 ~ flutter_gen ~

そんな問題を解決してくれるパッケージがあります！

それが[flutter_gen](https://pub.dev/packages/flutter_gen)です。

flutter_genはアセットを参照するコードを自動生成してくれるパッケージです。

## flutter_genのセットアップ⚙️

flutter_genを利用するにはプロジェクトのルートディレクトリにて

以下のコマンドを使います

`flutter pub add --dev build_runner flutter_gen_runner`　

ソースコード生成ツールの`build_runner`と

flutter_genのコードジェネレータである`flutter_gen_runner`を導入しています。

これで準備は完了です！

## 生成 & 利用🎉

実際に以下のコマンドを使ってコードを生成してみます。

`flutter packages pub run build_runner build`

すると、以下のようにディレクトリとファイルが生成されます。

![スクリーンショット 2024-11-24 11.40.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3634309/0d5ff384-a209-04f5-dd1e-52f7e58df9be.png)

ちなにみ、生成された内容はこんな感じです。

```dart
/// GENERATED CODE - DO NOT MODIFY BY HAND
/// *****************************************************
///  FlutterGen
/// *****************************************************

// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use

import 'package:flutter/widgets.dart';

class $AssetsImagesGen {
  const $AssetsImagesGen();

  /// File path: assets/images/check_mark.png
  AssetGenImage get checkMark =>
      const AssetGenImage('assets/images/check_mark.png');

  /// List of all assets
  List<AssetGenImage> get values => [checkMark];
}

class Assets {
  Assets._();

  static const $AssetsImagesGen images = $AssetsImagesGen();
}

class AssetGenImage {
  const AssetGenImage(
    this._assetName, {
    this.size,
    this.flavors = const {},
  });

  final String _assetName;

  final Size? size;
  final Set<String> flavors;

  Image image({
    Key? key,
    AssetBundle? bundle,
    ImageFrameBuilder? frameBuilder,
    ImageErrorWidgetBuilder? errorBuilder,
    String? semanticLabel,
    bool excludeFromSemantics = false,
    double? scale,
    double? width,
    double? height,
    Color? color,
    Animation<double>? opacity,
    BlendMode? colorBlendMode,
    BoxFit? fit,
    AlignmentGeometry alignment = Alignment.center,
    ImageRepeat repeat = ImageRepeat.noRepeat,
    Rect? centerSlice,
    bool matchTextDirection = false,
    bool gaplessPlayback = true,
    bool isAntiAlias = false,
    String? package,
    FilterQuality filterQuality = FilterQuality.low,
    int? cacheWidth,
    int? cacheHeight,
  }) {
    return Image.asset(
      _assetName,
      key: key,
      bundle: bundle,
      frameBuilder: frameBuilder,
      errorBuilder: errorBuilder,
      semanticLabel: semanticLabel,
      excludeFromSemantics: excludeFromSemantics,
      scale: scale,
      width: width,
      height: height,
      color: color,
      opacity: opacity,
      colorBlendMode: colorBlendMode,
      fit: fit,
      alignment: alignment,
      repeat: repeat,
      centerSlice: centerSlice,
      matchTextDirection: matchTextDirection,
      gaplessPlayback: gaplessPlayback,
      isAntiAlias: isAntiAlias,
      package: package,
      filterQuality: filterQuality,
      cacheWidth: cacheWidth,
      cacheHeight: cacheHeight,
    );
  }

  ImageProvider provider({
    AssetBundle? bundle,
    String? package,
  }) {
    return AssetImage(
      _assetName,
      bundle: bundle,
      package: package,
    );
  }

  String get path => _assetName;

  String get keyName => _assetName;
}

```

早速、生成されたファイルを使ってコードを書き換えてみます。

```dart
import 'package:flutter/material.dart';

import 'gen/assets.gen.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Display PNG Image'),
        ),
        body: Center(
          // child: Image.asset('assets/images/check_mark.png'),
          child: Image.asset(Assets.images.checkMark.path),
        ),
      ),
    );
  }
}
```
画像アセットのパスを直接書くのではなく、

生成されたファイル内にある`Assets`クラスを介して

呼び出したい画像アセットのパスを呼び出しています。

結果はこちらです。

![スクリーンショット 2024-11-24 12.08.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3634309/1c370071-4f31-abbb-e2f6-fa8bad3c7096.png)

ちゃんと表示されましたね🥳

## アセットのパスやファイル名を変えたい場合は...？

使っているアセットのリソースファイルのパスを変えたり、

ファイル名を変えたい場合はどうするのか、これもやってみようと思います。

方法は同じなので、今回はパスを変えた場合の時だけやってみようと思います。


### 手順1 ~ pubspec.yamlのアセットのパス（またはファイル名）を修正

`pubspec.yaml`に記載したアセットのパスを修正します。

```yaml
flutter:
  assets:
    - assets/check_mark.png
```

`pub get`も忘れずに！

### 手順2 ~ リソースファイルの配置（またはファイル名）を修正 ~

次に上記`pubspec.yaml`で指定した位置にアセットのリソースファイルを配置します。

ここまではflutter_genを使わない方法でも同じかと思います。

この時、`pubspec.yaml`で指定したアセットのパスやファイル名と、

実際のそれらが異なる場合は、手順3でコマンドが成功していても

コードがうまく生成されません。

![スクリーンショット 2024-11-24 16.57.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3634309/7e72d72d-a9a1-2215-d6f3-91c7458bc12d.png)

こんな内容が`pubspec.yaml`に表示されている場合は、

パスやファイル名が合っているか確認します。

修正後は`pub get`も忘れずに！


### 手順3 ~ コマンド実行~

先ほどのコード生成時に使ったコマンドと同じコマンドを使います。

`flutter packages pub run build_runner build`

この時、

```shell
[INFO] Building new asset graph completed, took 728ms
[INFO] Found 1 declared outputs which already exist on disk. This is likely because the`.dart_tool/build` folder was deleted, or you are submitting generated files to your source repository.
Delete these files?
1 - Delete
2 - Cancel build
3 - List conflicts
```

このような質問を返されると思いますので、

- どのファイルでコンフリクトが起こっているかを確認したい場合は「3」

- コンフリクトを起こしているファイルを消して再生成させたいなら「1」

- そもそも、このコマンドを中止したいなら「2」

上記数字を選択します。

今回は直接「1」を選択しています。

すると、同名のファイルが新しく生成されます。

内容はこちらです。

```dart
/// GENERATED CODE - DO NOT MODIFY BY HAND
/// *****************************************************
///  FlutterGen
/// *****************************************************

// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use

import 'package:flutter/widgets.dart';

class Assets {
  Assets._();

  static const AssetGenImage checkMark = AssetGenImage('assets/check_mark.png');

  /// List of all assets
  static List<AssetGenImage> get values => [checkMark];
}

class AssetGenImage {
  const AssetGenImage(
    this._assetName, {
    this.size,
    this.flavors = const {},
  });

  final String _assetName;

  final Size? size;
  final Set<String> flavors;

  Image image({
    Key? key,
    AssetBundle? bundle,
    ImageFrameBuilder? frameBuilder,
    ImageErrorWidgetBuilder? errorBuilder,
    String? semanticLabel,
    bool excludeFromSemantics = false,
    double? scale,
    double? width,
    double? height,
    Color? color,
    Animation<double>? opacity,
    BlendMode? colorBlendMode,
    BoxFit? fit,
    AlignmentGeometry alignment = Alignment.center,
    ImageRepeat repeat = ImageRepeat.noRepeat,
    Rect? centerSlice,
    bool matchTextDirection = false,
    bool gaplessPlayback = true,
    bool isAntiAlias = false,
    String? package,
    FilterQuality filterQuality = FilterQuality.low,
    int? cacheWidth,
    int? cacheHeight,
  }) {
    return Image.asset(
      _assetName,
      key: key,
      bundle: bundle,
      frameBuilder: frameBuilder,
      errorBuilder: errorBuilder,
      semanticLabel: semanticLabel,
      excludeFromSemantics: excludeFromSemantics,
      scale: scale,
      width: width,
      height: height,
      color: color,
      opacity: opacity,
      colorBlendMode: colorBlendMode,
      fit: fit,
      alignment: alignment,
      repeat: repeat,
      centerSlice: centerSlice,
      matchTextDirection: matchTextDirection,
      gaplessPlayback: gaplessPlayback,
      isAntiAlias: isAntiAlias,
      package: package,
      filterQuality: filterQuality,
      cacheWidth: cacheWidth,
      cacheHeight: cacheHeight,
    );
  }

  ImageProvider provider({
    AssetBundle? bundle,
    String? package,
  }) {
    return AssetImage(
      _assetName,
      bundle: bundle,
      package: package,
    );
  }

  String get path => _assetName;

  String get keyName => _assetName;
}
```

一見すると違いに気付きにくいかもしれませんが、

`Assets`クラスにあった`images`がなくなり、`checkMark`を介してパスを取得していますね。

では、生成したコードを呼び出している部分を見てみましょう。

![スクリーンショット 2024-11-24 16.35.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3634309/4c9c4265-f789-d5ec-764a-a2bf130bea2c.png)

このようにエラーが表示されているのが分かるかと思われます。

これがあることで、コードに問題があることに気付けますね。

実際に修正して実行してみましょう。

```dart
import 'package:flutter/material.dart';

import 'gen/assets.gen.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Display PNG Image'),
        ),
        body: Center(
          // child: Image.asset(Assets.images.checkMark.path),
          child: Image.asset(Assets.checkMark.path),
        ),
      ),
    );
  }
}
```

![スクリーンショット 2024-11-24 12.08.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3634309/3302ffab-3d4a-6562-9870-4c2e8aa9e00a.png)

ちゃんと表示されていますね🙌

## まとめ

今回はflutter_genを使ったアセット管理についてでした。

このパッケージを使うことで型安全にアセットを管理でき、

認知上の負荷を下げることに繋がりそうですね。

次回は@satoshihiraishiさんの

[『Quarkusで作ったアプリケーションをCloudRun関数で動かしてみる』](https://qiita.com/satoshihiraishi/items/b7e57957fb7e081d1823)

という記事です。

お楽しみに〜


## 参考

* [flutter_gen公式](https://pub.dev/packages/flutter_gen)

* [Flutter実践開発](https://gihyo.jp/book/2024/978-4-297-13993-3)

* [iconmonstr](https://iconmonstr.com/)