Skip to content

avoid_incorrect_image_opacity

v0.4.0 Warning Fix Widget Replacement

Flags Opacity widgets that wrap an Image widget as their child. The Image widget has a dedicated opacity parameter that is more efficient than wrapping it in a separate Opacity widget.

Opacity forces the compositor to paint its subtree into an offscreen buffer (saveLayer) and blend that buffer back — expensive, and worse the larger the image. Image takes an opacity animation of its own and applies it while painting the pixels, with no extra layer.

The quick fix moves the value inward, wrapping it in an AlwaysStoppedAnimation because Image.opacity takes an Animation<double>, not a plain double.

See also: Image.opacity | Opacity

// A watermark faded behind the page content.
Opacity(
opacity: 0.15,
child: Image.asset('assets/watermark.png'),
);
Image.asset(
'assets/watermark.png',
opacity: const AlwaysStoppedAnimation(0.15),
);

Image.network, Image.file, Image.memory and the unnamed Image(...) all take opacity:

// Don't
Opacity(
opacity: 0.8,
child: Image.network('https://example.com/avatar.png'),
);
// Do
Image.network(
'https://example.com/avatar.png',
opacity: const AlwaysStoppedAnimation(0.8),
);

If the value already comes from an Animation<double>, there is no wrapper to add — pass it straight through:

// Don't — Opacity forces a layer on every frame of the fade
Opacity(
opacity: fadeController.value,
child: Image.asset('assets/logo.png'),
);
// Do — no saveLayer, and the Image repaints without rebuilding
Image.asset('assets/logo.png', opacity: fadeController);

The quick fix would write AlwaysStoppedAnimation(fadeController.value) here, which is correct but freezes the fade at the current frame. Passing the animation itself is the better hand edit.

// Not reported — Text has no opacity parameter
Opacity(opacity: 0.5, child: const Text('Dimmed'));

The Image must be the direct child. Opacity > Padding > Image is not reported, because the padding would have to move too.

A subclass of Image is reported as well. The child is matched by assignability, so your own class CachedImage extends Image is caught — and it does inherit opacity, so the rewrite holds.

An Image that already sets opacity is skipped by the fix. The rule still reports the redundant Opacity, but combining two opacity sources is a decision, so nothing is offered automatically.

This rule is in the recommended preset, so it is on with preset: recommended or preset: opinionated. Add it to preset: core with avoid_incorrect_image_opacity: true.

To turn it off:

many_lints.yaml
rules:
avoid_incorrect_image_opacity: false

To keep the rule on but skip certain paths, use per-rule exclude.