skip to content

In Flutter, what do asset transformers declared in pubspec.yaml do, and how would you add one to shrink bundled JSON question packs?

level: seniorimportance: nice to knowfreq 18%

answer

  1. path: plus transformers: in pubspec
  2. dart run <package> --input --output
  3. chained in declared order
  4. non-zero exit fails the build
  5. FLUTTER_BUILD_MODE in the environment

basics

~20 s

An asset transformer is a Dart command-line package the flutter tool runs on an asset while bundling it, writing a transformed file under the same key. Declare it per asset with path: and transformers: in pubspec.yaml, and add the package as a dev dependency.

solid answer

~40 s

Since Flutter 3.22 an entry under `flutter: assets:` can be a map: `path:` plus `transformers:`, each item naming a `package:` and optional `args:`. During the build the tool runs `dart run <package> --input=<source> --output=<target>` plus the args, with `FLUTTER_BUILD_MODE` set to the build mode, and bundles the output under the original key. Transformers chain in declared order, and a non-zero exit or a missing output file fails the build with the transformer's output. The package must be resolvable by `dart run`, so it goes in `dev_dependencies`. The standard example is `vector_graphics_compiler`, which precompiles SVGs; for question packs I would write a small local package that minifies JSON and apply it to `path: assets/questions/`.

code

dart · 22 lines
dart
import 'dart:convert';
import 'dart:io';

void main(List<String> args) {
  String? valueOf(String name) {
    final prefix = '--$name=';
    for (final arg in args) {
      if (arg.startsWith(prefix)) return arg.substring(prefix.length);
    }
    return null;
  }

  final input = valueOf('input');
  final output = valueOf('output');
  if (input == null || output == null) {
    stderr.writeln('json_minifier: expected --input=<file> and --output=<file>');
    exit(64);
  }

  final decoded = jsonDecode(File(input).readAsStringSync());
  File(output).writeAsStringSync(jsonEncode(decoded));
}

go deeper

for a junior

Know that pubspec.yaml can attach a transformer package to an asset path and that the app still loads the asset by its original key.

for a middle

Explain the invocation, dart run with --input and --output, chaining order, and why the package belongs in dev_dependencies.

for a senior

Build a deterministic transformer, use FLUTTER_BUILD_MODE to keep debug builds fast, and make failures clear on CI.

for a principal

Decide which content processing belongs in the build, which in a separate content pipeline, and how to keep bundled output traceable to its source.

## What an asset transformer is An **asset transformer** is a Dart **command-line program**, published as a package, that the `flutter` tool runs on an asset **at build time**, before the asset is written into the bundle. The app still asks for the original key at runtime; it simply receives the transformed bytes. Transformers run on the developer's or CI machine, never on the device. Typical uses: - **`vector_graphics_compiler`** precompiles an SVG into a compact binary that the `vector_graphics` package draws without parsing XML at runtime. - Minifying or stripping JSON, such as removing editor notes from trivia question packs. - Optimising images or converting formats as part of the build. ## Declaring a transformer Instead of a plain string, the asset entry becomes a map with a `path` and a `transformers` list: ```yaml dev_dependencies: json_minifier: path: tool/json_minifier flutter: assets: - path: assets/questions/ transformers: - package: json_minifier - path: assets/logo.svg transformers: - package: vector_graphics_compiler args: ['--tessellate'] ``` - `path` can be a single file or a directory ending in `/`; for a directory the transformers apply to each file in it. - `package` names a package that `dart run` can execute, which means it must be a dependency of the app; `dev_dependencies` is the usual home, since nothing ships in the app's code. - `args` is an optional list of extra command-line arguments. - Several transformers **chain in the order declared**: each receives the previous one's output. The same map form also accepts `platforms:` (Flutter 3.41+, to bundle an asset only for some platforms) and `flavors:` (to tie assets to build flavors). ## How the flutter tool runs it For each asset and each transformer, the tool runs: ```bash dart run json_minifier --input=/path/to/source.json --output=/path/to/transformed.json ``` 1. Any `args` are appended after `--input` and `--output`. 2. The environment variable **`FLUTTER_BUILD_MODE`** is set to the build mode's command-line name (`debug`, `profile` or `release`), so a transformer can, for example, skip expensive work in debug builds. 3. A **non-zero exit code** fails the build, and the error message includes the command and the transformer's stdout and stderr. 4. If the transformer exits cleanly but writes **no output file**, the build fails as well. ## Writing a transformer package A transformer is an ordinary Dart package with an executable in `bin/` named after the package, so that `dart run <package>` finds it. Its `main` reads the `--input` and `--output` arguments, processes the file and writes the result. The code example is a minimal JSON minifier: it re-encodes the parsed JSON without whitespace. Keep transformers deterministic, since the same input should always produce the same bundled bytes, and print a clear message to `stderr` before exiting non-zero. ## The SVG case end to end The documented example pairs a transformer with a runtime package: 1. `pubspec.yaml` declares `- path: assets/logo.svg` with `transformers: - package: vector_graphics_compiler`. 2. At build time the compiler turns the SVG into a binary vector format and the tool bundles that output under the key `assets/logo.svg`. 3. At runtime the app draws it with the `vector_graphics` package, `VectorGraphic(loader: AssetBytesLoader('assets/logo.svg'))`, which reads the precompiled bytes instead of parsing XML. Choosing between runtime SVG parsing and precompiled vectors is a separate design question; the transformer is only the build-time half. ## Debugging a transformer - Run the same command by hand, `dart run <package> --input=... --output=...`, from the app's directory to see its output directly. - Compare the bundled file with the source when behaviour differs between the repository copy and the app. ## Consequences to keep in mind | Aspect | Effect | |---|---| | Runtime key | unchanged; code keeps loading `assets/questions/history.json` | | Build time | grows with the number of assets and the transformer's cost | | Debugging | the bundled file differs from the one in the repository | | CI | the transformer's dependencies must resolve on the build machine | ## When not to use one - Work that depends on the device, such as screen density, belongs at runtime or in resolution variants. - Content that changes after release cannot be transformed by the build; it needs a download pipeline. - A one-off conversion is simpler to run once and commit than to repeat on every build.

  • What does the app's code change after adding the minifying transformer to assets/questions/?
    Nothing. The transformer's output is bundled under the original key, so `rootBundle.loadString('assets/questions/history.json')` keeps working and simply receives the smaller file. Only the build changes: it now runs the transformer for every file in that directory.
  • How can a transformer do less work in debug builds?
    Read the `FLUTTER_BUILD_MODE` environment variable, which the flutter tool sets to `debug`, `profile` or `release` when it runs the transformer. A transformer can copy the input unchanged in debug mode for fast iteration and do the expensive optimisation only for `release`.

saying these in an interview costs you the question

  • Asset transformers run on the device when the asset is first loaded.
  • After transformation the app must load the asset under a new key.
  • Transformers listed for one asset run in parallel on the original file.
  • A failing transformer is skipped and the original file is bundled instead.
  • The transformer package must be a regular dependency shipped in the app.