はじめに
TRIAL&RetailAI Advent Calendar 2024 の 9日目の記事です。
昨日は@Carol_fanさんの『フロントエンドで自動ユニットテストについて』
という記事でした。
私も今、テストについて勉強をしているため、インプット&アウトプットをして
経験を重ねていきたいと思います!
さて、今回はFlutterでのアセット管理において、flutter_genという便利なパッケージが
ありますので、そのご紹介をしたいと思います!
自己紹介
TRIALの基盤チームにてモバイルアプリの開発に従事しています。
興味がある方は、以下をご参照下さい。
Retail AI:
https://www.retail-ai.jp/
TRIAL:
https://www.trial-net.co.jp/
採用ページ:
目次
アセットの追加
本記事の導入として、まずは一般的なアセットの追加方法について記載いたします。
手順は以下の通りです。
手順1
アセットを表示するために、まずはアセットを配置するディレクトリを
プロジェクトのルートディレクトリに作成します。
よくある命名はassets
かと思われますので、
今回はこのassets
ディレクトリに様々なアセットが配置されることを想定して
assets/images
といったようにimages
ディレクトリを作成して、
このimages
ディレクトリにあるpng形式の画像(check_mark.png
)を
表示させようと思います。
画像はiconmonstrにあるものを利用させていただいています。
手順2
次にpubspec.yaml
にて、flutter
セクションのサブセクションにassets
を追加して、
その下にアセットのパスを追加します。
flutter:
assets:
- assets/images/check_mark.png
pub get
も忘れずに。
これでアセットの準備は完了です。
手順3
このpng形式の画像を表示する簡単なサンプルを書いてみました。
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'),
),
),
);
}
}
結果はこちらです。
ちゃんと表示されていますね。
問題点...
上記のコードのImage.asset('assets/images/check_mark.png')
とのようにパスを直接指定することでもアセットを参照することはできるのですが、
例えばこれを、assets/check_mark.png
やassets/image/check_mark.png
といったようにTypoをした場合はアセットを参照できず、エラーになってしまいます...
上記のようなパスを直接記述するやり方ですと
アセットを配置するディレクトリが変わったり、
そのファイルの名前が変わったりした場合、
対応するコードとpubspec.yaml
の設定を修正しなければならず手間が掛かりますし、
また、コンパイルエラーも発生しないので、
「実行するまでエラーに気付かない」、といった問題も起こります...
解決策🔍 ~ flutter_gen ~
そんな問題を解決してくれるパッケージがあります!
それが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
すると、以下のようにディレクトリとファイルが生成されます。
ちなにみ、生成された内容はこんな感じです。
/// 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;
}
早速、生成されたファイルを使ってコードを書き換えてみます。
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
クラスを介して
呼び出したい画像アセットのパスを呼び出しています。
結果はこちらです。
ちゃんと表示されましたね🥳
アセットのパスやファイル名を変えたい場合は...?
使っているアセットのリソースファイルのパスを変えたり、
ファイル名を変えたい場合はどうするのか、これもやってみようと思います。
方法は同じなので、今回はパスを変えた場合の時だけやってみようと思います。
手順1 ~ pubspec.yamlのアセットのパス(またはファイル名)を修正
pubspec.yaml
に記載したアセットのパスを修正します。
flutter:
assets:
- assets/check_mark.png
pub get
も忘れずに!
手順2 ~ リソースファイルの配置(またはファイル名)を修正 ~
次に上記pubspec.yaml
で指定した位置にアセットのリソースファイルを配置します。
ここまではflutter_genを使わない方法でも同じかと思います。
この時、pubspec.yaml
で指定したアセットのパスやファイル名と、
実際のそれらが異なる場合は、手順3でコマンドが成功していても
コードがうまく生成されません。
こんな内容がpubspec.yaml
に表示されている場合は、
パスやファイル名が合っているか確認します。
修正後はpub get
も忘れずに!
手順3 ~ コマンド実行~
先ほどのコード生成時に使ったコマンドと同じコマンドを使います。
flutter packages pub run build_runner build
この時、
[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」を選択しています。
すると、同名のファイルが新しく生成されます。
内容はこちらです。
/// 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
を介してパスを取得していますね。
では、生成したコードを呼び出している部分を見てみましょう。
このようにエラーが表示されているのが分かるかと思われます。
これがあることで、コードに問題があることに気付けますね。
実際に修正して実行してみましょう。
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),
),
),
);
}
}
ちゃんと表示されていますね🙌
まとめ
今回はflutter_genを使ったアセット管理についてでした。
このパッケージを使うことで型安全にアセットを管理でき、
認知上の負荷を下げることに繋がりそうですね。
次回は@satoshihiraishiさんの
『Quarkusで作ったアプリケーションをCloudRun関数で動かしてみる』
という記事です。
お楽しみに〜