はじめに
- 「デザイナーさんから貰った画像を全部置けばいいんでしょ?」
- 「pubspec.yaml にパスをズラズラ書くのが面倒…」
これまではこれで動いていたので問題視していなかったのですが、改めて公式ドキュメントやエンジンのソースコードを読み込んでみると、**「なぜその解像度が必要なのか」「エンジンはどうやって画像を選んでいるのか」**という裏側のロジックが見えてきました。
本記事では、公式ドキュメントとFlutterエンジンのコードから学んだ「アセット画像の正しい配置」と、パフォーマンスを意識した「プロの画像管理術」を共有します。
1. 基本のおさらい:1.0x, 2.0x, 3.0x の正解ディレクトリ構造
まずは基本中の基本ですが、Flutterが期待しているディレクトリ構造です。
「1.5x」などの細かい刻みもありますが、実務的には以下の 3つのバリアント(1.0x, 2.0x, 3.0x) を用意するのがスタンダードかつ安全です。
assets/
images/
icon.png <-- Base (1.0x) / mdpi相当
2.0x/
icon.png <-- xhdpi相当 / @2x
3.0x/
icon.png <-- xxhdpi相当 / @3x
なぜ「1.5x」は作らなくていいの?
以前は 1.5x フォルダも推奨されていましたが、最近の開発現場(特にFigma運用)では 2.0x と 3.0x のみを書き出し、1.5x は用意しないケースが増えています。
これは、後述するFlutterの画像選択ロジックにより、「1.5x の端末では 2.0x の画像を縮小して表示したほうが(1.0xを拡大するより)綺麗だから」 という理由で代替可能だからです。
意外と知られていない pubspec.yaml の仕様
pubspec.yaml に画像を登録するとき、全ファイルを羅列していませんか?
YAML
\# 😓 頑張って全部書いている例
flutter:
assets:
\- assets/images/icon.png
\- assets/images/2.0x/icon.png
\- assets/images/3.0x/icon.png
実は、ディレクトリを指定するだけでOKです。しかも、解像度ごとのサブディレクトリ(2.0x/など)は書く必要がありません。
YAML
\# 😎 スマートな例
flutter:
assets:
\- assets/images/
ドキュメントによると、ディレクトリ指定の末尾に / を付けることでその直下のファイルが含まれます。そして解像度バリアント(Variant)だけは例外的に、サブディレクトリにあっても自動で認識される仕様になっています。これを知っているだけで pubspec.yaml の行数が劇的に減ります。
2. エンジンの気持ちになる:画像選択アルゴリズム _chooseVariant
ここからが本題です。「端末のピクセル比(Device Pixel Ratio: DPR)に合わせて、Flutterが勝手にいい感じの画像を選んでくれる」というのは知っていても、**「どういう基準で選んでいるのか」**まで意識したことはありますか?
Flutterフレームワークの image_resolution.dart にある _chooseVariant メソッドの挙動を追ってみました。
ルール1:DPR 2.0 未満の端末(低解像度)への配慮
コード内のコメントに興味深い記述があります。
kLowDprLimit = 2.0 という定数が定義されており、**DPRが2.0未満のスクリーンでは「高解像度の画像を優先して選ぶ」**傾向があります 2。
- 理由: 低解像度の画面で小さな画像を無理やり拡大(アップスケーリング)すると、ボケが目立って品質が下がるためです。
- 挙動: 例えば DPR 1.5 の端末の場合、1.5x の画像が存在しなければ、近い数値の 1.0x ではなく、あえて解像度の高い 2.0x を選んで縮小(ダウンスケーリング)して表示します。
これが、先ほど述べた「1.5x フォルダは必須ではない」という運用の根拠です。
ルール2:DPR 2.0 以上の端末(高解像度)の挙動
DPRが2.0以上の高精細ディスプレイでは、単純に数値が最も近いバリアントが選ばれます。例えば DPR 2.7 の端末(最近のPixelなど)であれば、2.0x よりも 3.0x が選ばれます。
ここから言える「配置の戦略」
最近のスマホはDPR 3.0以上が当たり前になりつつありますが、タブレットやデスクトップなどでは 1.0 や 2.0 の環境も現役です。
「容量削減のために 3.0x だけ置いておけば、あとは縮小されるからいいや」という戦略をとると、低スペック端末が巨大な 3.0x 画像を読み込んでメモリ不足に陥るリスクがあります。
逆に 1.0x 画像を置かない(2.0xと3.0xのみ)という運用もよく見かけますが、これは理にかなっています。DPR 1.0の端末は今どきほぼ存在しないため、2.0x がフォールバックとして機能するからです。
3. 「高画質ならOK」は危険! 画像1枚でメモリ16MB消費する罠
「高画質な画像を縮小して表示すれば綺麗でしょ?」
これは画質の面では正解ですが、パフォーマンスの面では大罪になり得ます。
衝撃の計算式
Flutter(Skia/Impeller)が画像をデコードしてメモリ(ヒープ/Native Memory)に展開するとき、ファイルサイズ(KB)ではなく**解像度(ピクセル数)**がメモリ消費量を決めます。
$$\text{メモリ消費量} = \text{横px} \times \text{縦px} \times 4 \text{バイト (RGBA)}$$
例えば、リスト表示用のサムネイルとして、将来を見越して 2000px × 2000px の画像を 3.0x フォルダに入れたとします。
- 消費メモリ: $2000 \times 2000 \times 4 = 16,000,000$ バイト $\approx$ 16 MB
たった1枚のアイコンで16MBです。これを ListView で50個並べたら…?
一瞬で数百MBのメモリを食い潰し、古いiPhoneやAndroidでは OutOfMemory でクラッシュします
解決策:cacheWidth / cacheHeight
この問題を解決するために、Image.asset には cacheWidth / cacheHeight というパラメータがあります。
Dart
Image.asset(
'assets/images/huge\_photo.png',
// width: 100, // これはレイアウト上のサイズ(論理ピクセル)
// ↓ ここが重要!
// 実際にメモリに展開するサイズ(物理ピクセル)を指定する。
// 「表示サイズ(100) × DPR」を指定するのが最適解。
cacheWidth: (100 \* MediaQuery.of(context).devicePixelRatio).round(),
)
これを使うと、エンジンは元の2000pxの画像を、指定したサイズ(例:300px)まで縮小しながらデコードします。メモリ消費量は劇的に下がります。
リストビューで画像を表示する場合は、この指定はほぼ必須と言っても過言ではありません
まとめ:明日から使えるチェックリスト**
Flutterのドキュメントと内部コードを読んでわかった、「プロのアセット管理」チェックリストです。
- ディレクトリ構造: 基本は 1.0x, 2.0x, 3.0x。1.5x は 2.0x で代用できるので無理に作らなくて良い。
- pubspec記述: ディレクトリ指定(assets/images/)だけでOK。バリアントフォルダまで書かない。
- メモリ対策: 大きな画像(特に写真系)を表示するときは、必ず cacheWidth で 表示サイズ × DPR を指定してメモリ使用量を抑える。
普段何気なく使っている機能でも、ドキュメントを深掘りすると「なぜそうなっているのか」という理由が見えてきます。
皆さんのFlutterライフが少しでも快適になれば幸いです!
---
参考文献