skip to content

In Flutter, what do MaterialApp and Scaffold each provide, and why does Scaffold.of(context) sometimes fail to find the Scaffold?

level: juniorimportance: must knowfreq 70%

answer

  1. app level vs screen level
  2. Navigator, Theme, localizations, messenger
  3. appBar, body, FAB, drawer, bottom bar
  4. context above the Scaffold
  5. Builder or a separate widget

basics

~20 s

MaterialApp sets up app-wide services: the Navigator, the Theme, Material localizations and a root ScaffoldMessenger. Scaffold lays out one screen's Material slots. Scaffold.of fails when its context sits above the Scaffold, such as the widget that builds it.

solid answer

~40 s

`MaterialApp` is the app-level shell: it creates the `Navigator` and routes, applies `theme`, `darkTheme` and `themeMode` (`ThemeMode.system` by default), installs the Material localizations, and wraps everything in a root `ScaffoldMessenger` for snack bars. `Scaffold` is the per-screen layout: `appBar`, `body`, `floatingActionButton`, `drawer` and `endDrawer`, `bottomNavigationBar`, `bottomSheet` and `persistentFooterButtons`, and it is where snack bars appear. `Scaffold.of(context)` walks up from the given context, so calling it with the context of the widget whose `build` returns the `Scaffold` fails with 'Scaffold.of() called with a context that does not contain a Scaffold.', because that context is above it. I fix it with a `Builder` inside the Scaffold, by extracting the child into its own widget, or with `Scaffold.maybeOf` when absence is acceptable.

code

dart · 42 lines
dart
import 'package:flutter/material.dart';

void main() => runApp(const BikeRentalApp());

class BikeRentalApp extends StatelessWidget {
  const BikeRentalApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Bike Rental',
      theme: ThemeData(colorSchemeSeed: Colors.teal),
      home: const RentalHome(),
    );
  }
}

class RentalHome extends StatelessWidget {
  const RentalHome({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Nearby bikes')),
      endDrawer: const Drawer(child: Center(child: Text('Filters'))),
      body: Center(
        // Builder gives a context below the Scaffold; `context` here is above it.
        child: Builder(
          builder: (innerContext) => OutlinedButton(
            onPressed: () => Scaffold.of(innerContext).openEndDrawer(),
            child: const Text('Filters'),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () {},
        icon: const Icon(Icons.qr_code_scanner),
        label: const Text('Scan to unlock'),
      ),
    );
  }
}

go deeper

for a junior

Recall that MaterialApp is app-wide (navigator, theme, localizations, messenger) and Scaffold is per screen, with slots such as appBar, body, floatingActionButton and bottomNavigationBar.

for a middle

Explain why Scaffold.of fails with the build method's own context and the fixes: a Builder, a separate widget, a GlobalKey, or maybeOf when absence is normal.

for a senior

Show judgement about structure: one MaterialApp, one Scaffold per route, nested Scaffolds only with care, and messengers or keys chosen for who needs to reach the Scaffold.

for a principal

Decide how the app shell is composed, which services live above every route and which per screen, so that features plug into one Scaffold contract.

## MaterialApp: the app-level shell `MaterialApp` is usually the root widget of a Material app. It is not a visible widget; it sets up services that every screen below it relies on: - a **`Navigator`** with the `home`, `routes` or `onGenerateRoute` you give it (`MaterialApp.router` plugs in a `RouterConfig` such as a `GoRouter` instead); - the **theme**: `theme`, `darkTheme` and `themeMode`, which defaults to `ThemeMode.system`; - **localizations** for Material widgets, so dialogs and pickers have their labels; - a root **`ScaffoldMessenger`**, which is why `ScaffoldMessenger.of(context).showSnackBar(...)` works from any screen; - debug aids such as `debugShowCheckedModeBanner`, `true` by default. Most apps have exactly one `MaterialApp`. Nesting a second one creates a second navigator and theme, which is rarely what anyone wants. ## Scaffold: one screen's layout `Scaffold` implements the basic Material screen structure. Each screen, typically each route, has its own `Scaffold`, and it places widgets into named slots: | Slot | Typical content | |---|---| | `appBar` | an `AppBar` with title and actions | | `body` | the screen's main content | | `floatingActionButton` | the primary action, such as 'Scan to unlock' | | `drawer`, `endDrawer` | side panels opened by swipe or button | | `bottomNavigationBar` | a `NavigationBar` | | `bottomSheet` | a persistent sheet anchored to the bottom | | `persistentFooterButtons` | buttons that stay above the bottom bar | The `Scaffold` also displays the snack bars and material banners its nearest `ScaffoldMessenger` sends it. ## The Material 3 AppBar in brief With Material 3, the default since Flutter 3.16, an `AppBar` sits flat at elevation `0.0` and rises to its `scrolledUnderElevation`, `3.0` by default, when content scrolls underneath it. Its toolbar height resolves from the widget, then the `AppBarTheme`, then `kToolbarHeight`, 56 logical pixels. `centerTitle` follows the platform when not set: centred on iOS and macOS when there are fewer than two actions, start-aligned elsewhere. ## Why Scaffold.of fails `Scaffold.of(context)` searches the **ancestors** of `context` for a `ScaffoldState`. A widget's `BuildContext` is its own position in the tree, so the `context` passed to a `build` method that *returns* a `Scaffold` is above that Scaffold, not inside it. The classic failure: 1. `RentalHome.build(context)` returns a `Scaffold`. 2. A button in its `body` calls `Scaffold.of(context).openEndDrawer()` using that same `context`. 3. The search starts above the Scaffold and finds none: 'Scaffold.of() called with a context that does not contain a Scaffold.' Fixes: - wrap the button in a `Builder`, whose `builder` gets a context below the Scaffold; - move the button into its own widget class, which gets its own context below the Scaffold; - keep a `GlobalKey<ScaffoldState>` on the Scaffold and call `key.currentState`; - use `Scaffold.maybeOf(context)` when the caller may legitimately have no Scaffold. Snack bars used to have the same trap, because they were shown through `Scaffold.of`. Since Flutter 2.0 they go through `ScaffoldMessenger.of`, and `MaterialApp` provides a messenger above every route, so any context under the app works. ## A bike-rental home screen The home screen of a bike-rental app is one `Scaffold`: an `AppBar` titled 'Nearby bikes', a map in the `body`, a 'Scan to unlock' `FloatingActionButton.extended`, an `endDrawer` with filters, and a `NavigationBar` in `bottomNavigationBar` for Map, Rides and Account. The `MaterialApp` above it supplies the theme, the navigator that will push the ride-details route, and the messenger that shows 'Bike reserved' snack bars. ## Common structural mistakes - **A second `MaterialApp` inside a screen.** It brings its own `Navigator` and theme, so routes pushed inside it stay inside it and the outer theme stops applying below it. - **A screen with no `Scaffold` or other `Material` ancestor.** Widgets such as `ListTile`, `Checkbox` and `InkWell` run a debug check and fail with 'No Material widget found.', because they need a Material surface to draw ink on. - **Nested Scaffolds without intent.** An inner Scaffold inside a tab is legitimate, but each registered Scaffold under the same messenger shows a snack bar at the same time, so the message can appear twice. - **A `GlobalKey<ScaffoldState>` created inside `build`.** A new key on every build gives the Scaffold a new identity each time and loses its state; create the key once in a `State` field.

  • Why can ScaffoldMessenger.of(context) use a context that Scaffold.of(context) cannot?
    Because the messenger lives higher up. `MaterialApp` inserts a root `ScaffoldMessenger` above every route, so any context inside the app finds it, even the one whose `build` returns the `Scaffold`. `Scaffold.of` needs a context below that particular Scaffold. The messenger then forwards the snack bar to the Scaffold currently registered with it.
  • When would you give a Scaffold a GlobalKey instead of using Scaffold.of?
    When code outside the Scaffold's subtree must open its drawer or query it, for example a parent widget or a controller that holds the key. It avoids hunting for a context below the Scaffold, but a `GlobalKey` must be created once and kept, not rebuilt in `build`, and it ties the caller to one specific Scaffold.

saying these in an interview costs you the question

  • Wrap every route in its own MaterialApp to give it a fresh theme.
  • Scaffold.of(context) works with the context of the build method that returns the Scaffold.
  • MaterialApp draws the app bar, so screens do not need a Scaffold.
  • Show snack bars with Scaffold.of(context).showSnackBar.
  • A Material 3 AppBar always has a shadow, even with nothing scrolled under it.