Liquid Glass for Flutter: refraction, blur, tint and a rim over the live
backdrop, in the shape the engine already draws (RSuperellipse). Built for
cost first. Features come second.
The name is glass, spelled in digits.
Status: 0.x. The API will still change between minor versions; every change is in the changelog.
Every loop above is rendered by the package itself, headless, from
example/showcase/; the tiles share one backdrop and meet without a seam.
To re-shoot them after a change, run tool/showcase.sh — see
Re-shooting the animations.
Most glass packages put a BackdropFilter on every surface, which means the
engine reads the backdrop once per surface on every frame. This package takes a
different route:
- One host, one capture. A
GlassHostrecords what is painted under all of its glass into one atlas, at a resolution picked against a measured quality budget. Every surface then samples its own slot of that atlas. - A capture only when something changed. The host walks the composited layer tree. When nothing under the glass changed, it keeps the proxy it already has. That covers a still screen, and a moving glass over content that stays put. Keeping the proxy is the default, and it is the biggest saving in the package: 79.4% and 66.3% of the route's added cost on Adreno, 97.8% on Metal.
- The blur is a downscale. A 1/N proxy is a Gaussian of σ ≈ N/2 to within about 1%, at a fraction of the price of a real Gaussian.
Measured costs, each on its own platform, because the platforms are not comparable:
| platform | glass vs stock Material | engine BackdropFilter.grouped |
|---|---|---|
| Android, Impeller/Vulkan, Adreno 830 (GPU cycles) | ×0.99…1.08 | ×1.78…3.15 |
| iPad, Impeller/Metal (GPU ms, scrolling screen) | ×1.93…2.02, or ×1.46…1.54 with thermal throttling | — |
flutter pub add g1455Requires Flutter 3.47 or later.
import 'package:g1455/g1455.dart';
GlassHost(
child: Stack(
children: <Widget>[
Positioned.fill(child: content), // what the glass refracts
Positioned(
top: 16, left: 16, right: 16,
child: GlassBar(child: Text('Library')), // the glass
),
],
),
)What is in the box:
GlassSurfaceis the primitive: a region of the screen that is glass, with a corner radius, an optional finish, andpresence/materializefor appearing.GlassBar,GlassButtonandGlassCardare panels with a label colour chosen for legibility.GlassSwitch,GlassSliderandGlassTabBarare controls whose knob or selection turns into a clear drop while held.GlassGroupandGlassUniondraw several surfaces as one silhouette.GlassTraveldeclares the region a moving glass travels in, so the motion does not trigger a capture.GlassFinish.regularDark,.regularLight,.clearand.frostedare the optics, calibrated against Apple's own materials on iOS 26. Apple's.regularis two materials, dark over dark content and light over light, andGlassHostpicks the branch the same way unless you name one: frombackdropand the platform's appearance.- Glass appears and leaves through
GlassSurface.materialize: the bend, the blur and the tint arrive over the whole shape.presenceerodes the shape and is for budding inside aGlassGroup; on a lone panel it narrows to a line. GlassTier/GlassTierPolicypick the rung: full glass, a flat translucent fill, or opaque. Use it for reduce transparency, low-end devices and thermal pressure.GlassLedgerreports how much glass is on the screen and what it costs.GlassRippleis optional and not Apple's. When the glass is touched, a viscous wave spreads from the touch: a dimple under the finger, a front that travels out, and a spring-back on release.viscositygoes from water (0) to honey (1). Declare it onGlassHost.ripplefor every surface, or onGlassSurface.ripplefor one. A wave takes no capture and repaints nothing, and a surface with no wave runs the same shader as before. It is off under reduced motion.
The example/ directory has a full app: four pages under one host,
and a settings menu that switches the finish, the tint, the rung and the ripple.
The package cannot work some things out from the render tree, so the application declares them:
- What is behind the glass, for label legibility:
GlassHost.backdropfor a flat colour, orrichBackdrop: truewithminLabelContrastfor an image or a scrolling feed. Without either, labels are picked against the worst case, and in debug the package warns when a finish cannot be read over it. - Reduce transparency, increase contrast on macOS, and thermal state.
Flutter does not pass these on, and this package ships no platform code to
read them. Read them natively and pass them in: reduce transparency to
GlassTierPolicy, contrast toGlassHost.highContrast, thermal state toGlassHost.thermalas aGlassThermalState. On iOS and Android 34+ the host already reads contrast fromMediaQuery. On macOS the engine does not pass it on. - The hardware family, if it is not an Apple device:
GlassHost.hardware. Undeclared hardware gets the same behaviour with no price attached.
The route renders byte-identically on Impeller (Metal, Vulkan, GLES) and on
Skia/GLES, which Android falls back to below API 29 and on Vivante GPUs. It also
works on web, with both CanvasKit and Skwasm. Every bundled shader is compiled
for all five shader targets in test/shader_targets_test.dart, so a shader that
SkSL would reject fails the tests rather than a user's app.
package:g1455/glass_diagnostics.dart exposes the switches a benchmark flips,
such as the tile split of a group's draw and the anti-alias flag. It also
exposes the host's proxy handle, whose counters show whether a frame captured.
An application has no reason to import it.
flutter pub get
dart format .
flutter analyze --fatal-infos
flutter test --coverage
(cd example && flutter test test/ showcase/)dart format takes its width (120) and its trailing-comma rule from
analysis_options.yaml, and CI fails on a file it would change.
Some tests check a constant baked into lib/ against the measurement it came
from. Those measurements are copied into provenance/, unchanged and under
their original file names. They live in the repository only; the published
package does not carry them.
A pull request runs the same checks in CI, plus flutter pub publish --dry-run
and pana. A release is a tag: bump version in pubspec.yaml, add its
## <version> section to CHANGELOG.md, and push v<version>. The tag
publishes to pub.dev and opens a GitHub release with that section as notes.
tool/showcase.sh # every scene
tool/showcase.sh switch,tab_bar # just theseThe script plays each scene of example/showcase/scenes.dart under
flutter test, with a fake clock and scripted touches, so every run produces
the same frames and needs no device. It plays one loop to let springs and waves
settle and records the next, and it fails if the last frame does not lead back
into the first. It then packs the frames into looping webp files in
doc/showcase/ with cwebp and webpmux (from libwebp: brew install webp),
each frame encoding only the rect that changed.
The screenshots pub.dev shows are the same loops at half the size, because pub ships them with the package:
SHOWCASE_DPR=1 SHOWCASE_DIR=doc/screenshots tool/showcase.shA new scene is a ShowcaseScene in that list: a builder given the loop's phase
from 0 to 1, and Strokes for the fingers. Scenes are cut from one tall
backdrop in list order, so a scene's position in the list is its place in the
strip.







