build method
Describes the part of the user interface represented by this widget.
The framework calls this method when this widget is inserted into the tree in a given BuildContext and when the dependencies of this widget change (e.g., an InheritedWidget referenced by this widget changes). This method can potentially be called in every frame and should not have any side effects beyond building a widget.
The framework replaces the subtree below this widget with the widget returned by this method, either by updating the existing subtree or by removing the subtree and inflating a new subtree, depending on whether the widget returned by this method can update the root of the existing subtree, as determined by calling Widget.canUpdate.
Typically implementations return a newly created constellation of widgets that are configured with information from this widget's constructor and from the given BuildContext.
The given BuildContext contains information about the location in the tree at which this widget is being built. For example, the context provides the set of inherited widgets for this location in the tree. A given widget might be built with multiple different BuildContext arguments over time if the widget is moved around the tree or if the widget is inserted into the tree in multiple places at once.
The implementation of this method must only depend on:
- the fields of the widget, which themselves must not change over time, and
- any ambient state obtained from the
contextusing BuildContext.dependOnInheritedWidgetOfExactType.
If a widget's build method is to depend on anything else, use a StatefulWidget instead.
See also:
- StatelessWidget, which contains the discussion on performance considerations.
Implementation
@override
Widget build(BuildContext context) {
// 1. Resolve Settings
// In grouped mode, the explicit `settings` field is a const placeholder;
// we must inherit the real settings from the ancestor layer.
final inherited =
context.dependOnInheritedWidgetOfExactType<InheritedLiquidGlass>();
final baseSettings =
(!useOwnLayer && inherited != null) ? inherited.settings : settings;
// ---- MINIMAL FAST-PATH ---------------------------------------------------
// GlassQuality.minimal bypasses all custom shaders. Renders via
// _FrostedFallback: ClipPath(ShapeBorderClipper) + BackdropFilter + tint.
// ClipPath correctly clips ALL shape types (oval, superellipse, rect).
// Zero fragment shader cost on any device.
//
// platformViewBackdrop ALSO routes here: over a PlatformView only a live
// BackdropFilter samples the composited map. The premium/standard shaders
// read a captured backdrop that EXCLUDES the platform view (see
// canUsePremiumShader below), so they render inert there — the frost is the
// one tier that actually blurs over a PlatformView. This finally delivers
// the "live BackdropFilter path" the canUsePremiumShader comment promises.
// --------------------------------------------------------------------------
if (quality == GlassQuality.minimal ||
baseSettings.effectiveBlur == 0 ||
platformViewBackdrop) {
return _wrapWithDecorations(
context,
baseSettings,
_FrostedFallback(
shape: shape,
settings: baseSettings,
clipBehavior: clipBehavior,
glowIntensity: glowIntensity,
isAccessibilityFallback: false,
isInteractive: isInteractive,
platformViewBackdrop: platformViewBackdrop,
child: child,
),
);
}
// ---- IP1: ACCESSIBILITY FAST-PATH ----------------------------------------
// iOS 26 glass degrades to a solid frosted panel when "Reduce Transparency"
// is enabled. We honour the equivalent Flutter signal (highContrast, which
// is the closest available platform proxy for isReduceTransparencyEnabled).
//
// When triggered, the entire glass shader pipeline is bypassed. The fallback
// is a ClipRRect + BackdropFilter(blur) + semi-opaque tinted container —
// still visually layered, but with no refraction, no specular, and no
// chromatic aberration. Zero GPU shader cost.
//
// GlassAccessibilityScope must be in the widget tree for this to activate;
// without it, defaults.reduceTransparency = false and we proceed normally.
// --------------------------------------------------------------------------
final accessibilityData = GlassAccessibilityData.of(context);
if (accessibilityData.reduceTransparency) {
return _wrapWithDecorations(
context,
baseSettings,
_FrostedFallback(
shape: shape,
settings: baseSettings,
clipBehavior: clipBehavior,
glowIntensity: glowIntensity,
isAccessibilityFallback: true,
isInteractive: isInteractive,
child: child,
),
);
}
// If we are on Skia/Web, we CANNOT use LiquidGlass.withOwnLayer or withOwnLayer
// because those will fall back to FakeGlass (solid color) inside the renderer.
// We MUST use our LightweightLiquidGlass to get actual glass effects.
// platformViewBackdrop forces the live BackdropFilter path even at premium:
// the premium shader's toImageSync backdrop can't capture a PlatformView, so
// over one it must use BackdropFilter (live) instead. The local/cheap checks
// are evaluated before the platform shader-support query (_canUseImpeller).
final bool canUsePremiumShader = !kIsWeb &&
!platformViewBackdrop &&
quality == GlassQuality.premium &&
_canUseImpeller;
if (!canUsePremiumShader) {
// 1. Detect Grouped Elevation
// When a parent provides the blur (Batch-Blur Optimization), we lose the
// "double-darkening" effect of nested blurs. We compensate with the
// densityFactor parameter (0.0-1.0) which triggers synthetic density physics
// in the shader to make elevated widgets "pop" against the background.
final bool shouldElevate =
allowElevation && (inherited?.isBlurProvidedByAncestor ?? false);
// Calculate density factor for shader (0.0 = normal, 1.0 = elevated)
final double densityFactor = shouldElevate ? 1.0 : 0.0;
// Normalise settings for the 2D lightweight shader to prevent it from looking
// overpowering when the user has tuned their settings for the 3D premium shader.
//
// BYPASS: When quality is explicitly GlassQuality.standard, the settings
// are already calibrated for the Standard renderer — skip normalization.
// Normalization only makes sense when adapting Premium-tuned settings to
// Standard; if the caller already knows they're on Standard, their values
// must be passed through unchanged so tuning sliders take full effect.
final bool skipNormalization = quality == GlassQuality.standard;
final LiquidGlassSettings normalizedSettings;
if (skipNormalization) {
normalizedSettings = baseSettings.copyWith(
glassColor: baseSettings.glassColor.withValues(
alpha: (baseSettings.glassColor.a *
baseSettings.standardOpacityMultiplier)
.clamp(0.0, 1.0),
),
);
} else {
// Frosting normalization: adapts Premium settings for the 2D shader.
// Thickness scaled down (2D inner shadows look much thicker than 3D bevels).
// Light intensity scaled down (2D gradients look brighter than 3D speculars).
normalizedSettings = baseSettings.copyWith(
thickness: (baseSettings.effectiveThickness * 0.4)
.clamp(0.0, double.infinity),
lightIntensity:
(baseSettings.effectiveLightIntensity * 0.6).clamp(0.0, 10.0),
glassColor: baseSettings.glassColor.withValues(
alpha: (baseSettings.glassColor.a *
baseSettings.standardOpacityMultiplier)
.clamp(0.0, 1.0),
),
);
}
// Apply subtle elevation boost to settings (preserves saturation!)
final color = normalizedSettings.effectiveGlassColor;
final effectiveSettings = shouldElevate
? LiquidGlassSettings(
glassColor:
color, // Removed flat +0.2 alpha boost for predictability
refractiveIndex: normalizedSettings.refractiveIndex,
thickness: normalizedSettings.effectiveThickness,
lightAngle: normalizedSettings.lightAngle,
lightIntensity: (normalizedSettings.effectiveLightIntensity * 1.2)
.clamp(0.0, 10.0),
chromaticAberration: normalizedSettings.chromaticAberration,
blur: normalizedSettings.effectiveBlur,
visibility: normalizedSettings.visibility,
saturation: normalizedSettings.effectiveSaturation,
ambientStrength:
(normalizedSettings.effectiveAmbientStrength * 0.4)
.clamp(0.0, 1.0),
glowIntensity: normalizedSettings.glowIntensity,
// Preserve whiten through the elevation rebuild; otherwise the
// whitening would silently drop to 0 for grouped/elevated
// surfaces such as bars.
whitenStrength: normalizedSettings.whitenStrength,
whitenGated: normalizedSettings.whitenGated,
)
: normalizedSettings;
// If this is a container (allowElevation=false), we are providing a blur
// for all our children to use. We update the InheritedLiquidGlass tree.
if (!allowElevation) {
return _wrapWithDecorations(
context,
baseSettings,
LightweightLiquidGlass(
shape: shape,
settings: effectiveSettings,
densityFactor: 0.0, // Containers are never elevated
glowIntensity: 0.0, // Containers don't glow
child: InheritedLiquidGlass(
settings: effectiveSettings,
quality: quality,
isBlurProvidedByAncestor: true,
child: child,
),
),
);
}
// Elevated widgets use PATH B (no backgroundKey). They composite via
// SrcOver against the container's output.
final Widget lightweightWidget = LightweightLiquidGlass(
shape: shape,
settings: effectiveSettings,
densityFactor: densityFactor, // 0.0 or 1.0 based on elevation
glowIntensity:
glowIntensity * 0.35, // Normalise additive glow to match Impeller
child: child,
);
return _wrapWithDecorations(context, baseSettings, lightweightWidget);
}
// Impeller + Premium Path: Use the renderer's native path.
// Wrap in PremiumGlassTracker so GlassPerformanceMonitor can correlate
// slow raster frames with active premium surfaces.
//
// Force useOwnLayer when inside a GlassIsolationScope (e.g. GlassScaffold
// bottom bar). This gives bars their own compositing layer so body glass
// cards don't composite over bar buttons.
//
// NOTE: isInteractive is NOT included here. It only controls
// RepaintBoundary wrapping (lines below). Including it would force
// every GlassButton into its own compositing layer, breaking grouped
// rendering inside bars (e.g. BottomBarExtraBtn must blend with the
// tab pill, not render as a separate glass surface). Buttons that need
// independent refraction should set useOwnLayer: true explicitly.
//
// De-isolate children of the own-layer so nested glass (e.g. tab
// items inside a bottom bar) groups with this layer rather than
// creating additional own-layers (which would cause double-glass).
final effectiveUseOwnLayer =
useOwnLayer || GlassIsolationScope.isIsolated(context);
if (effectiveUseOwnLayer) {
// Resolve shadows for the GPU cutout method
final isDark = GlassTheme.brightnessOf(context) == Brightness.dark;
// Fallback to the CSS-style shadow for platforms with known saveLayer Impeller bugs
final useFallbackShadow =
kIsWeb || defaultTargetPlatform == TargetPlatform.windows;
final shadows =
(isDark || _FrostedFallback._isFlatEdge(shape) || useFallbackShadow)
? const <BoxShadow>[]
: baseSettings.effectiveShadow;
Widget premium = LiquidGlass.withOwnLayer(
shape: shape,
settings: settings,
shadows: shadows,
clipBehavior: clipBehavior,
clipExpansion: clipExpansion,
// De-isolate children so nested glass groups with this own-layer
// rather than creating its own (which causes double-glass).
// Carry the parent's defaultQuality through so quality hints
// (e.g. premium for bars) are preserved even when de-isolated.
child: GlassIsolationScope(
isolated: false,
defaultQuality: GlassIsolationScope.defaultQualityOf(context),
child: child,
),
);
final premiumTracker = _wrapWithBacker(
baseSettings,
PremiumGlassTracker(
child: premium,
),
);
// If we bypassed the GPU cutout shadow, apply the standard CSS-style shadow instead
if (useFallbackShadow &&
baseSettings.effectiveShadow.isNotEmpty &&
!isDark &&
!_FrostedFallback._isFlatEdge(shape)) {
return _wrapWithLightModeShadow(context, baseSettings, premiumTracker);
}
return premiumTracker;
} else {
// Grouped elements (e.g. inside GlassBottomBar) rely on the ancestor's
// LiquidGlassLayer to provide the RepaintBoundary and BackdropGroup.
// IMPORTANT: Do NOT wrap grouped elements with the shadow Stack — it
// inserts a widget between the grouped glass and its ancestor blend
// group, breaking metaball morphing (the blend SDF pass requires
// grouped render objects to be direct descendants of the shared layer).
return PremiumGlassTracker(
child: LiquidGlass.grouped(
shape: shape,
clipBehavior: clipBehavior,
child: child,
),
);
}
}