build method

  1. @override
Widget build(
  1. BuildContext context
)
override

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:

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) {
  final mediaQuery = MediaQuery.of(context);
  final topPad = mediaQuery.padding.top;
  final botPad = mediaQuery.padding.bottom;

  // Resolve effective bar heights.
  // If appBar implements PreferredSizeWidget, use its preferred height;
  // otherwise fall back to the explicit appBarHeight parameter.
  final effectiveAppBarHeight = appBar is PreferredSizeWidget
      ? (appBar! as PreferredSizeWidget).preferredSize.height
      : appBarHeight;
  final effectiveBottomBarHeight = bottomBar is PreferredSizeWidget
      ? (bottomBar as PreferredSizeWidget).preferredSize.height
      : (bottomBar != null ? (bottomBarHeight ?? 60.0) : 0.0);

  // Resolve edge fade toggles.
  final doFadeTop = topEdgeFade ?? (edgeFade && appBar != null);
  final doFadeBottom = bottomEdgeFade ?? (edgeFade && bottomBar != null);

  // Calculate fade heights.
  // Only include appBarHeight when an appBar is present — without one, the
  // fade covers just the status bar area + extent.
  final topFadeHeight = topPad +
      (appBar != null ? effectiveAppBarHeight : 0.0) +
      topEdgeFadeExtent;
  final bottomFadeHeight =
      effectiveBottomBarHeight + botPad + bottomEdgeFadeExtent;

  // Build the body content.
  Widget bodyContent = body;

  // Wrap with edge fading if enabled.
  if (extendBody && (doFadeTop || doFadeBottom)) {
    bodyContent = GlassScrollEdgeEffect(
      topFadeHeight: topFadeHeight,
      bottomFadeHeight: bottomFadeHeight,
      fadeTop: doFadeTop,
      fadeBottom: doFadeBottom,
      style: edgeStyle,
      // Pass the explicit background colour so the async-capture fallback
      // gradient uses the correct colour in dark mode instead of defaulting
      // to CupertinoTheme.scaffoldBackgroundColor (which is near-black).
      fadeColor: backgroundColor,
      child: bodyContent,
    );
  }

  // Wrap in GlassContentAwareContent when content-aware brightness is on.
  // This installs the RepaintBoundary that the scope captures.
  if (contentAwareBrightness) {
    bodyContent = GlassContentAwareContent(child: bodyContent);
  }

  // Build the Stack with guaranteed z-ordering.
  // All conditional children have explicit Keys so that Flutter can track
  // them by identity rather than position. Without keys, toggling header
  // (null → widget → null) shifts the index of appBar and bottomBar,
  // causing Flutter to unmount/remount them — losing animation state.
  final stackChildren = <Widget>[
    // 1. Body (bottom of stack — always below bars).
    if (extendBody)
      Positioned.fill(child: bodyContent)
    else
      Positioned(
        top: appBar != null ? topPad + effectiveAppBarHeight : 0,
        left: 0,
        right: 0,
        bottom: bottomBar != null ? effectiveBottomBarHeight + botPad : 0,
        child: bodyContent,
      ),

    // 2. Body overlays (between body and bars — e.g. floating play pill).
    if (bodyOverlays != null) ...bodyOverlays!,

    // 2b. Fixed header — fades on scroll (e.g. "Listen Now" in Apple Music).
    // IgnorePointer is only active when opacity == 0 (fully faded) so that
    // tappable elements inside the header work at full visibility.
    if (header != null)
      Positioned(
        key: const ValueKey('glass_scaffold_header'),
        top: 0,
        left: 0,
        right: 0,
        child: SafeArea(
          bottom: false,
          child: headerScrollController != null
              ? AnimatedBuilder(
                  animation: headerScrollController!,
                  builder: (context, child) {
                    // Guard: during AnimatedSwitcher cross-fades, both old
                    // and new scroll views briefly share the same controller.
                    // Reading .offset (or .positions.last) throws if there
                    // are multiple positions. Falling back to 0.0 for that
                    // brief ~300 ms window is correct UX — the header just
                    // stays fully visible during the transition.
                    final offset = headerScrollController!.hasClients &&
                            headerScrollController!.positions.length == 1
                        ? headerScrollController!.offset
                        : 0.0;
                    // Guard against divide-by-zero if headerFadeDistance == 0.
                    final opacity = headerFadeDistance > 0
                        ? (1.0 - offset / headerFadeDistance).clamp(0.0, 1.0)
                        : (offset > 0 ? 0.0 : 1.0);
                    return IgnorePointer(
                      // Disable hit-testing only when fully invisible.
                      ignoring: opacity == 0.0,
                      child: Opacity(opacity: opacity, child: child),
                    );
                  },
                  child: header!,
                )
              : header!,
        ),
      ),

    // 2. App bar (above body — painted after body in Stack).
    // Uses _GlassIsolationScope to tell descendant glass widgets
    // (GlassButton, AdaptiveGlass) to render with their own independent
    // glass layer instead of sharing the page-level layer. This prevents
    // body glass cards from compositing over the app bar's glass buttons,
    // and ensures the glass background paints in the correct Z-order.
    // If isolated were false, the button's background would paint in the
    // page's blend group (which is behind the body text), while the button's
    // foreground would paint here, causing visual tearing.
    if (appBar != null)
      Positioned(
        key: const ValueKey('glass_scaffold_app_bar'),
        top: 0,
        left: 0,
        right: 0,
        child: GlassIsolationScope(
          isolated: true,
          defaultQuality: GlassQuality.premium,
          child: appBar!,
        ),
      ),

    // 3. Bottom bar (above body — painted after body in Stack).
    // SafeArea ensures the bar is never obscured by the Android system
    // navigation bar or the iOS home indicator on any device.
    if (bottomBar != null)
      Positioned(
        key: const ValueKey('glass_scaffold_bottom_bar'),
        left: 0,
        right: 0,
        bottom: 0,
        child: SafeArea(
          top: false,
          left: false,
          right: false,
          // iOS 26 floats pills natively over the home indicator visually
          // (the 20px gap straddles the indicator).
          // Android uses a physical nav bar or gesture bar that requires
          // being pushed up explicitly.
          bottom: defaultTargetPlatform == TargetPlatform.android,
          child: GlassIsolationScope(
            isolated: true,
            defaultQuality: GlassQuality.premium,
            child: bottomBar!,
          ),
        ),
      ),
  ];

  // Resolve the system UI overlay style for the AnnotatedRegion.
  // This ensures the status bar icons are correctly styled even on routes
  // pushed via CupertinoPageRoute (which manages its own AnnotatedRegion).
  // Without this, pages without a GlassAppBar lose the status bar icons
  // because CupertinoPageRoute's default region overrides the imperative
  // SystemChrome call from GlassPage.
  final bool useLightIcons = switch (statusBarStyle) {
    GlassStatusBarStyle.light => true,
    GlassStatusBarStyle.dark => false,
    GlassStatusBarStyle.auto =>
      MediaQuery.platformBrightnessOf(context) == Brightness.dark,
    GlassStatusBarStyle.none => true, // doesn't matter — no region
  };

  Widget stackWidget =
      Stack(clipBehavior: Clip.none, children: stackChildren);

  // Wrap in GlassContentAwareScope when content-aware brightness is on.
  // The scope must be an ancestor of both the sampled body
  // (GlassContentAwareContent) and the adaptive controls (bars with
  // adaptiveBrightness: true).
  if (contentAwareBrightness) {
    stackWidget = GlassContentAwareScope(child: stackWidget);
  }

  // Resolve effective background: explicit widget > backgroundColor colour >
  // null (Scaffold inherits Theme.scaffoldBackgroundColor).
  final Widget? effectiveBackground = background ??
      (backgroundColor != null
          ? SizedBox.expand(
              child: ColoredBox(color: backgroundColor!),
            )
          : null);

  Widget scaffold = CupertinoPageScaffold(
    // Only force transparent when GlassPage will render a background widget
    // behind the scaffold. When no background is provided, null lets
    // CupertinoPageScaffold use the CupertinoTheme default (opaque) so the
    // page is not see-through during route transitions (issue #177).
    backgroundColor:
        effectiveBackground != null ? const Color(0x00000000) : null,
    resizeToAvoidBottomInset: resizeToAvoidBottomInset ?? true,
    child: stackWidget,
  );

  // Wrap in AnnotatedRegion so the status bar style sticks even on
  // CupertinoPageRoute transitions that manage their own region.
  if (statusBarStyle != GlassStatusBarStyle.none) {
    scaffold = AnnotatedRegion<SystemUiOverlayStyle>(
      value: useLightIcons
          ? SystemUiOverlayStyle.light
          : SystemUiOverlayStyle.dark,
      child: scaffold,
    );
  }

  return GlassPage(
    background: effectiveBackground,
    settings: settings,
    // GlassScaffold handles AnnotatedRegion itself, so tell GlassPage to
    // skip its own wrap + imperative SystemChrome call.
    statusBarStyle: GlassStatusBarStyle.none,
    edgeToEdge: edgeToEdge,
    themeOverride: themeOverride,
    enableBackgroundSampling: enableBackgroundSampling,
    child: scaffold,
  );
}