Liquid Glass Easy

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.

SetupWhat the lens does
Impeller — default on iOS, Android and desktopRefracts the live backdrop: whatever your app painted behind it. No view, no background widget, no setup.
Skia inside a LiquidGlassViewRefracts the view’s captured backgroundWidget, wherever the lens sits inside the view’s child.
Skia with no viewDegrades to a frosted surface — blur, tint, rim — and logs a one-time debug notice.

Performance by engine and platform#

Impeller is the best engine for the glass — in performance and visually. The lens reads the backdrop the GPU already has, so a frame costs one shader pass per lens (one per batch, with LiquidGlassBatch) and nothing is rasterized twice. Skia has to produce the backdrop as a texture first, and that capture is the whole of its cost. How much it hurts depends on where Skia is running.

WherePerformanceWhy
Impeller — iOS, Android, desktopBestLive backdrop, no capture. Many lenses stay cheap inside a batch.
Skia — desktopStrongest SkiaA fast GPU and plenty of memory absorb the capture; full glass at pixelRatio: 1.0 with real-time capture is comfortable.
Skia — mobileGoodThe capture is affordable for a few lenses. Lower pixelRatio and realTimeCapture: false for static backgrounds keep it smooth on older phones.
Skia — webWeakestThe capture crosses the browser’s WebGL path every frame, so it is the slowest place for full glass. Keep lens counts low, drop pixelRatio, and prefer lite glass for anything dense.

Blur then shader, or shader then blur#

The two engines look almost the same, and the one difference you can see comes from the order in which the blur and the refraction shader run. On Impeller the backdrop is blurred first and the shader runs on the blurred pixels: the refraction, the magnification and the rim all bend an already-soft image, so the edge of the glass is as soft as its interior and the whole piece reads as one material.

On Skia the order is reversed: the shader runs first on the sharp capture, and the blur is applied afterwards — and only to the interior, so the border of the glass is not blurred. The refracted band along the edge keeps its detail, which makes a Skia rim look a touch crisper and more defined than the same style on Impeller. It is a small difference and the same LiquidGlassStyle gives the same shape, tint and refraction on both; it is only the softness of the edge that moves. The one exception on Skia is when you use the blender: the LiquidGlassBlender shader blurs the lenses itself, so their border is blurred too and the edge looks the same as on Impeller.

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.

ParameterDefaultWhat it controls
backgroundWidget—The layer being captured. Lenses refract this, not the child.
childnullYour UI. Anything here can hold a lens, at any depth.
pixelRatio1.0Resolution of the capture. Below 1 is cheaper and softer; the glass hides a surprising amount of it.
realTimeCapturetrueRe-capture every frame. Turn it off for a background that does not move.
useSynctrueKeeps the capture in step with the frame being built. Off is cheaper and can lag by a frame.
refreshRatedeviceRefreshRateCaps how often the capture runs, independent of the display.
useImpellerBackdropnullOverrides engine auto-detection. Leave it null unless you are testing the other path on purpose.
controllernullA LiquidGlassViewController, for captureOnce() — one snapshot on your cue — and for driving adaptivity by hand.
adaptiveSamplingnullBoth 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.
batchtrueImpeller 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.
regionCapturefalseCaptures 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.

Dart
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. LiquidGlassAdaptivity judges the background by reading its pixels, and reading pixels means a capture. Impeller has none: it never takes one. So a LiquidGlassView with adaptiveSampling set 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 ratio 0.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.

Dart
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // Every program at once: the lens's, and the blender's.
  await LiquidGlassShaders.ensureLoaded();
  runApp(const MyApp());
}
  • General: useSync: true, pixelRatio: 0.8–1.0.
  • Performance-first: useSync: false, pixelRatio: 0.5–0.7.
  • Static background: realTimeCapture: false beats every other tuning knob combined.

Comments

Comments are GitHub Discussions — reply from either place.