Engines and the view
Impeller reads the backdrop. Skia needs a capture. Same widgets.
Refraction needs the pixels behind the glass. Impeller can hand a shader the live backdrop; Skia cannot. That single difference is the whole story, and LiquidGlassView is the answer to it.
| Setup | What the lens does |
|---|---|
| Impeller — iOS/Android default | Refracts the live backdrop: whatever your app painted behind it. No view, no background widget, no setup. |
Skia inside a LiquidGlassView | Refracts the view’s captured backgroundWidget, wherever the lens sits inside the view’s child. |
| Skia with no view | Degrades to a frosted surface — blur, tint, rim — and logs a one-time debug notice. |
LiquidGlassView#
The view is the capture pipeline: it rasterizes backgroundWidget into a texture and hands that texture to every lens inside child. Its full parameter list is generated from the source on the API page; these are the ones you will actually reach for.
| Parameter | Default | What it controls |
|---|---|---|
backgroundWidget | — | The layer being captured. Lenses refract this, not the child. |
child | null | Your UI. Anything here can hold a lens, at any depth. |
pixelRatio | 1.0 | Resolution of the capture. Below 1 is cheaper and softer; the glass hides a surprising amount of it. |
realTimeCapture | true | Re-capture every frame. Turn it off for a background that does not move. |
useSync | true | Keeps the capture in step with the frame being built. Off is cheaper and can lag by a frame. |
refreshRate | deviceRefreshRate | Caps how often the capture runs, independent of the display. |
useImpellerBackdrop | null | Overrides engine auto-detection. Leave it null unless you are testing the other path on purpose. |
controller | null | A LiquidGlassViewController, for captureOnce() — one snapshot on your cue — and for driving adaptivity by hand. |
adaptiveSampling | null | Both engines. Turns on the tiny background-luminance capture LiquidGlassAdaptivity reads. Null means no sampling at all, so adaptive surfaces inside fall back to a guess. One capture serves every adaptive surface in the view, and it is idle-gated: a still screen samples nothing. |
batch | true | Impeller only. Puts the lenses in child on one shared read of the backdrop, and those in backgroundWidget on another. Inert on the capture path, which already reads once per view. See Batch. |
regionCapture | false | Captures each lens’s own rect instead of the whole background — a win when a small lens sits on a large background, a loss when many lenses are spread across it. Skia useSync path only. |
Capture once, or every frame#
realTimeCapture: true rasterizes the background every frame. That is right for a scrolling feed, a video, an animating gradient — anything genuinely moving. It is pure waste for a background that never changes.
When the background is static, take one snapshot and refract it forever. Moving a lens over a fixed image costs nothing extra: the shader re-runs, but nothing re-rasterizes.
final controller = LiquidGlassViewController();
LiquidGlassView(
controller: controller,
backgroundWidget: const MyBackground(),
realTimeCapture: false,
child: const MyGlassUI(),
);
// Re-snapshot by hand after the background actually changes:
await controller.captureOnce();This page's demo runs a background that never stops sliding. Set the capture to frozen and the glass goes on refracting bands that have already moved on, while the ones around it carry on — the offset between them is how old the snapshot has become, and capture once closes it.
When Impeller still wants a view#
On Impeller a lens needs nothing: it reads the live backdrop wherever it sits. So the view looks like a Skia formality — and it is, for refraction. It is not for everything else the view provides, and two of those are worth reaching for on Impeller too.
- `adaptiveSampling` — the one that is not optional.
LiquidGlassAdaptivityjudges the background by reading its pixels, and reading pixels means a capture. Impeller has none: it never takes one. So aLiquidGlassViewwithadaptiveSamplingset is how adaptive glass gets something to judge on either engine — without it, every adaptive surface inside falls back to guessing from the app theme. It is one tiny capture (pixel ratio0.05, 8 per second) shared by every adaptive surface in the view, and it stops entirely while the screen is still. - `batch` — Impeller-only by nature. The view batches its lenses by default, which is a pure Impeller saving: it is the capture path that already reads once per view, while on Impeller each lens copies the backdrop for itself unless a batch says otherwise.
- One tree, both engines. A view costs nothing on Impeller beyond the widget itself, so writing the page inside one means the same code runs on a phone and on the web without a branch.
Compile the shaders before the first frame#
This one is not about the capture, and it is not about the engine either — it applies to both. The glass programs are compiled once per run of the app, by whichever lens mounts first, and a lens whose program has not arrived yet paints its frosted fallback until it does. LiquidGlassShaders.ensureLoaded() in main() moves that compile ahead of the first frame.
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Every program at once: the lens's, and the blender's.
await LiquidGlassShaders.ensureLoaded();
runApp(const MyApp());
}Recommended settings#
- General:
useSync: true,pixelRatio: 0.8–1.0. - Performance-first:
useSync: false,pixelRatio: 0.5–0.7. - Static background:
realTimeCapture: falsebeats every other tuning knob combined.
Comments
Comments are GitHub Discussions — reply from either place.