Flutter + FVM で開発環境のバージョンを固定する の次に、プロジェクトを少し触り始めた段階で後手に回りやすいのがアセット参照です。Image.asset('assets/...') や SvgPicture.asset('assets/...') を画面ごとに書き始めると、rename やディレクトリ整理のたびに文字列を追いかけることになります。この記事では flutter_gen を最小構成で導入し、PNG 画像、SVG イラスト、SVG アイコンを型安全に参照する流れを整理します。
1. ゴールと非対象
対象読者
- Flutter の環境構築と FVM による SDK 固定までは終わっている人
- 画面実装へ進む前に、画像や SVG の置き場所と参照方法をそろえたい人
- アセットパスの文字列直書きを早めにやめたい人
この記事で到達する状態
flutter_gen_runnerとbuild_runnerを使ってassets.gen.dartを生成できる- PNG 画像を
Assets.images.*.image()で表示できる - SVG イラストと SVG アイコンを
Assets.*.svg()で表示できる - アセット名を変更したとき、どこを更新すべきか判断できる
非対象
- ランチャーアイコン生成
- カスタムフォント管理
- 画像圧縮やアセット最適化
- デザインシステム全体の設計
今回は「Flutter アプリ内で使うアセット参照をそろえる」ことに絞ります。素材作成や配布用アイコンの話は含めません。
2. 先にアセット管理の流れを掴む
今回の流れは次の 4 段階です。
flowchart LR
A[assets/images や assets/icons にファイルを置く] --> B[pubspec.yaml に登録する]
B --> C[dart run build_runner build]
C --> D[lib/gen/assets.gen.dart が生成される]
D --> E[Widget から Assets.images.* や Assets.icons.* を呼ぶ]
ここで見ておきたいのは、Widget が直接ファイルパス文字列を持たない点です。
- ファイル配置:
assets/配下の責務 - 生成対象の宣言:
pubspec.yamlの責務 - 型付き参照の作成:
build_runnerの責務 - 画面表示:
lib/main.dartの責務
この分け方にしておくと、あとで assets/icons/scan.svg を assets/icons/barcode_scan.svg に変えたときも、古い文字列が画面のどこかへ残る状態を減らせます。
3. プロジェクトを作成し、必要なパッケージを追加する
3-1. 環境構築がまだなら先に済ませる
環境構築がまだの場合は Windows 11で始めるFlutter開発環境 を先に参照してください。
3-2. Flutter プロジェクトを作成する
次のコマンドでプロジェクトを作成します。
flutter create my_asset_app
cd my_asset_app
3-3. エミュレーターを起動する
利用可能なエミュレーターを確認します。
flutter emulators
表示された ID を指定して起動します。
flutter emulators --launch <emulator_id>
3-4. パッケージを追加する
今回は SVG 表示と生成処理のために 3 つ追加します。
flutter pub add flutter_svg
flutter pub add --dev build_runner
flutter pub add --dev flutter_gen_runner
役割は次の通りです。
flutter_svg: SVG ファイルを画面へ表示するbuild_runner: 生成処理を実行するflutter_gen_runner: assets 参照用の Dart コードを生成する
依存を増やしすぎると、最初の 1 本としては何が必要なのか見えにくくなります。ここでは flutter_gen の入口に必要な組み合わせだけに絞ります。
4. pubspec.yaml と assets ディレクトリを整える
4-1. pubspec.yaml の assets 設定を更新する
依存の追加は直前のコマンドで済ませているので、ここでは flutter: と flutter_gen: の設定を加えます。次の pubspec.yaml では、生成対象ディレクトリと出力先をまとめて決めています。どの設定が参照範囲を増やし、どの設定が生成コードの振る舞いを決めるかに注目してください。
pubspec.yaml の該当部分を次のように更新します。
flutter:
uses-material-design: true
generate: true
assets:
- assets/images/
- assets/illustrations/
- assets/icons/
flutter_gen:
output: lib/gen/
integrations:
flutter_svg: true
コードのポイント
① generate: true で生成コードを有効にする
flutter:
uses-material-design: true
generate: true
この指定がないと、flutter_gen を使う前提の生成処理が動きません。まず Flutter 側で「このプロジェクトは生成コードを使う」と宣言している行です。
② assets: に生成対象のディレクトリを並べる
assets:
- assets/images/
- assets/illustrations/
- assets/icons/
どのフォルダを getter 化したいかをここで決めます。ディレクトリ構成を先にそろえておくと、後で Assets.images... のように辿りやすい名前になります。
③ flutter_svg: true で SVG を .svg() から呼べる形に寄せる
flutter_gen:
output: lib/gen/
integrations:
flutter_svg: true
SVG を PNG と別の呼び出し API にしたい意図がこの設定に現れています。出力先の lib/gen/ もここで固定されるため、生成コードを探す場所がぶれません。
4-2. assets ディレクトリを作り、サンプル素材を置く
次のコマンドでディレクトリを作成します。
macOS / Linux / Git Bash:
mkdir -p assets/images assets/illustrations assets/icons
PowerShell:
mkdir assets/images, assets/illustrations, assets/icons
作成後の構成は次の通りです。
my_asset_app/
├─ assets/
│ ├─ images/
│ │ └─ warehouse_banner.png
│ ├─ illustrations/
│ │ └─ empty_box.svg
│ └─ icons/
│ ├─ scan.svg
│ └─ package.svg
├─ lib/
└─ pubspec.yaml
assets/images/warehouse_banner.png には任意の PNG 画像を 1 枚置いてください。手元のスクリーンショットやダミー画像で問題ありません。この記事の主題は画像素材の作り方ではなく、置き場所と参照方法です。
assets/illustrations/empty_box.svg の内容は次の通りです。
<svg width="240" height="180" viewBox="0 0 240 180" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="20" y="40" width="200" height="100" rx="16" fill="#F4F1E8"/>
<path d="M40 50L120 80L200 50" stroke="#8C6A43" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M40 50V100L120 130L200 100V50" stroke="#8C6A43" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M120 80V130" stroke="#8C6A43" stroke-width="10" stroke-linecap="round"/>
</svg>
assets/icons/scan.svg には次を使います。
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 6H4V18" stroke="#0F766E" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M38 6H44V18" stroke="#0F766E" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 42H4V30" stroke="#0F766E" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M38 42H44V30" stroke="#0F766E" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="12" y="22" width="24" height="4" rx="2" fill="#0F766E"/>
</svg>
assets/icons/package.svg も同じく SVG で作成します。内容は次の通りです。
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 16L24 8L40 16" stroke="#B45309" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 16V34L24 42L40 34V16" stroke="#B45309" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M24 8V24" stroke="#B45309" stroke-width="4" stroke-linecap="round"/>
<path d="M24 24L40 16" stroke="#B45309" stroke-width="4" stroke-linecap="round"/>
</svg>
4-3. 生成コードを作る
設定とファイル配置が終わったら、プロジェクト直下で次を実行します。
flutter pub get
dart run build_runner build --delete-conflicting-outputs
実行後に lib/gen/assets.gen.dart が生成されます。このファイルを直接編集する必要はありません。ファイル名やディレクトリが変わったら、再度 build_runner を回して更新します。
ここで一度、生成された getter 名を軽く見ておくと後半が読みやすくなります。
assets/images/warehouse_banner.png->Assets.images.warehouseBannerassets/illustrations/empty_box.svg->Assets.illustrations.emptyBoxassets/icons/scan.svg->Assets.icons.scan
ファイル名がそのまま getter 名に近い形へ変換されるので、ディレクトリ構成と命名を最初にそろえる意味が出てきます。
5. lib/main.dart で PNG / SVG / アイコンを表示する
5-1. lib/main.dart を次の内容で作成する
この lib/main.dart は、PNG、SVG イラスト、SVG アイコンを Assets 経由で呼び出す例です。文字列でパスを書かずに、生成 getter から UI を組む流れを確認します。
lib/main.dart は次の内容で作成します。
import 'package:flutter/material.dart';
import 'package:my_asset_app/gen/assets.gen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'flutter_gen asset demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF0F766E)),
scaffoldBackgroundColor: const Color(0xFFF7F4ED),
),
home: const AssetCatalogPage(),
);
}
}
class AssetCatalogPage extends StatelessWidget {
const AssetCatalogPage({super.key});
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('flutter_gen でアセット管理'),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
Text(
'assets の参照を文字列で散らさず、画像・SVG・アイコンを 1 つの入り口から読むサンプルです。',
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 16),
ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Assets.images.warehouseBanner.image(
height: 180,
width: double.infinity,
fit: BoxFit.cover,
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'PNG 画像',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.primary,
),
),
const SizedBox(height: 8),
Text(Assets.images.warehouseBanner.path),
],
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: <Widget>[
Assets.illustrations.emptyBox.svg(
width: 120,
height: 90,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'SVG イラスト',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.primary,
),
),
const SizedBox(height: 8),
Text(Assets.illustrations.emptyBox.path),
const SizedBox(height: 8),
const Text(
'空状態や説明カードのように、拡大縮小しても荒れにくい素材を置く場所として分けています。',
),
],
),
),
],
),
),
const SizedBox(height: 16),
Text(
'SVG アイコン',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.primary,
),
),
const SizedBox(height: 12),
const AssetActionCard(
title: 'バーコード読取',
description: 'scan.svg を使う操作カード',
icon: _AssetIcon.scan,
),
const SizedBox(height: 12),
const AssetActionCard(
title: '出荷確認',
description: 'package.svg を使う操作カード',
icon: _AssetIcon.package,
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFE7F2EF),
borderRadius: BorderRadius.circular(20),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const <Widget>[
Text(
'参照先を 1 箇所に寄せる利点',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text('・画面側がファイルパス文字列を持たない'),
Text('・assets の整理と Widget の修正箇所を切り分けやすい'),
Text('・rename 後に古い getter が残ればコンパイル時に気づきやすい'),
],
),
),
],
),
);
}
}
enum _AssetIcon {
scan,
package,
}
class AssetActionCard extends StatelessWidget {
const AssetActionCard({
required this.title,
required this.description,
required this.icon,
super.key,
});
final String title;
final String description;
final _AssetIcon icon;
Widget _buildIcon() {
switch (icon) {
case _AssetIcon.scan:
return Assets.icons.scan.svg(width: 28, height: 28);
case _AssetIcon.package:
return Assets.icons.package.svg(width: 28, height: 28);
}
}
String _buildPath() {
switch (icon) {
case _AssetIcon.scan:
return Assets.icons.scan.path;
case _AssetIcon.package:
return Assets.icons.package.path;
}
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: <Widget>[
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
color: const Color(0xFFF2ECE1),
borderRadius: BorderRadius.circular(14),
),
alignment: Alignment.center,
child: _buildIcon(),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(description),
const SizedBox(height: 6),
Text(
_buildPath(),
style: const TextStyle(
fontSize: 12,
color: Color(0xFF475569),
),
),
],
),
),
],
),
);
}
}
コードのポイント
① PNG は .image() で呼び出す
child: Assets.images.warehouseBanner.image(
height: 180,
width: double.infinity,
fit: BoxFit.cover,
),
PNG は Image 系の Widget を返す .image() で使います。ファイルパス文字列ではなく getter から辿れるため、画像の置き場と呼び出し側の対応が追いやすくなります。
② SVG は .svg() で呼び出し API が分かれる
Assets.illustrations.emptyBox.svg(
width: 120,
height: 90,
),
SVG は .svg() として明示的に呼ぶので、PNG と見分けやすくなります。どのアセットが flutter_svg 経由なのかをコード上で判断しやすいのが利点です。
③ アイコン表示でも path 文字列を直書きしない
switch (icon) {
case _AssetIcon.scan:
return Assets.icons.scan.svg(width: 28, height: 28);
case _AssetIcon.package:
return Assets.icons.package.svg(width: 28, height: 28);
}
アイコンの選択も Assets.icons.* に寄せているため、画面側で生のファイルパスを持たずに済みます。リネーム時に古い参照へコンパイルエラーで気づけるのも、この形の利点です。
5-2. flutter run で起動する
コードを保存したら、プロジェクト直下で次を実行します。
flutter run
アプリが起動すると、上から順に PNG 画像、SVG イラスト、SVG アイコン付きカードが並びます。
6. 文字列直書きと何が違うかを整理する
たとえば、生成コードを使わない場合は次のような書き方になります。
Image.asset('assets/images/warehouse_banner.png')
SvgPicture.asset('assets/icons/scan.svg')
これでも動きますが、アセット名を変えたあとで古い文字列が残っていても、該当画面を開くまで気づきにくくなります。flutter_gen を通すと次の形になります。
Assets.images.warehouseBanner.image()
Assets.icons.scan.svg()
この形にしておくと、整理しやすくなるのは次の点です。
- 画面側が「どのディレクトリの、どのアセットか」を getter 名で読める
- rename 後に getter 名が変われば、修正漏れをコンパイル時に拾いやすい
assets/images、assets/illustrations、assets/iconsの責務を分けやすい
逆に、flutter_gen を入れてもディレクトリ構成とファイル名が雑なままだと、Assets.misc.a1 のような読みにくい参照が増えるだけです。効果が出るのは、配置規則と命名規則を一緒にそろえたときです。
7. つまずきやすい点だけ先に整理する
pubspec.yaml のインデントがずれて読めない
flutter: の下に generate: と assets: を同じ階層で置けているかを確認してください。YAML のインデントが 2 つずれるだけで、生成以前に pub get が止まります。
getter 名が増えない
ファイルを追加したあとに dart run build_runner build --delete-conflicting-outputs を実行していない可能性があります。名前変更や追加のたびに、生成処理を回し直してください。
.svg() が使えない
flutter_svg の追加と flutter_gen.integrations.flutter_svg: true の両方が必要です。どちらか片方だけだと、SVG を flutter_gen 経由で扱う形になりません。
8. まとめ
flutter_gen の最小構成を入れておくと、画像・SVG・アイコンの参照を assets.gen.dart へ寄せられます。今回の段階で必要なのは、assets の置き場所、pubspec.yaml の設定、生成コマンド、Widget 側の呼び出し方の 4 点です。これだけ固めておけば、後続の UI 記事や API 記事でアセットパスの文字列を毎回書き散らさずに済みます。
次は Flutterで最初に詰まりやすいDartの書き方 に進むと、生成コードを含む Flutter のサンプルを読みやすくなります。画面構築へ先に進みたい場合は、そのまま Flutterのレイアウト入門(Column / Row / Stack の使い分け) へつなげても問題ありません。