Flutter Widgets Fundamentals

Scaffold, AppBar & Body

50 min Lesson 6 of 18

Understanding the Scaffold Widget

The Scaffold widget is one of the most essential widgets in Flutter. It provides the basic visual structure for a Material Design app, including slots for the app bar, body, floating action button, drawer, bottom navigation bar, and bottom sheet. Almost every Flutter screen you build will use a Scaffold as its root layout widget.

Think of Scaffold as the skeleton of your app screen. It handles positioning of common UI elements so you can focus on the actual content.

Basic Scaffold Structure

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('My First Scaffold'),
        ),
        body: const Center(
          child: Text('Hello, Scaffold!'),
        ),
      ),
    );
  }
}

The Scaffold widget accepts many named parameters. Here are the most commonly used ones:

  • appBar -- A widget displayed at the top of the screen, usually an AppBar.
  • body -- The primary content of the screen, displayed below the app bar.
  • floatingActionButton -- A button that floats above the body, typically for the primary action.
  • drawer -- A side panel that slides in from the left (or right in RTL).
  • bottomNavigationBar -- A navigation bar displayed at the bottom of the screen.
  • bottomSheet -- A persistent sheet displayed at the bottom of the screen.
  • backgroundColor -- The background color of the entire scaffold.

Scaffold with All Major Properties

Let us build a more complete example that demonstrates the key Scaffold properties together:

Complete Scaffold Example

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
        backgroundColor: Colors.indigo,
        foregroundColor: Colors.white,
      ),
      body: const Center(
        child: Text(
          'Welcome to the app!',
          style: TextStyle(fontSize: 24),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          debugPrint('FAB pressed!');
        },
        child: const Icon(Icons.add),
      ),
      floatingActionButtonLocation:
          FloatingActionButtonLocation.centerFloat,
      backgroundColor: Colors.grey[100],
    );
  }
}
Note: The floatingActionButtonLocation property lets you control where the FAB is positioned. Options include endFloat (default, bottom-right), centerFloat (bottom-center), endDocked, centerDocked, and more.

Deep Dive into AppBar

The AppBar widget is the toolbar displayed at the top of the screen. It is the most common widget used in the appBar slot of a Scaffold. AppBar provides areas for a title, navigation icons, and action buttons.

AppBar Properties Overview

AppBar(
  // The main title widget
  title: const Text('App Title'),

  // Widget on the left (back button auto-added with Navigator)
  leading: IconButton(
    icon: const Icon(Icons.menu),
    onPressed: () {},
  ),

  // Widgets on the right side
  actions: [
    IconButton(
      icon: const Icon(Icons.search),
      onPressed: () {},
    ),
    IconButton(
      icon: const Icon(Icons.more_vert),
      onPressed: () {},
    ),
  ],

  // Elevation (shadow depth)
  elevation: 4.0,

  // Colors
  backgroundColor: Colors.deepPurple,
  foregroundColor: Colors.white,

  // Center the title (default varies by platform)
  centerTitle: true,
)

Key AppBar properties explained:

  • title -- The primary widget displayed in the app bar. Usually a Text widget, but can be any widget (a search field, logo, etc.).
  • leading -- A widget displayed before the title. If null and there is a Drawer, a menu icon is auto-added. If the current route can be popped, a back arrow is auto-added.
  • actions -- A list of widgets displayed after the title. Typically IconButton widgets for toolbar actions.
  • elevation -- The z-coordinate of the app bar, controlling the shadow. Set to 0 for a flat look.
  • centerTitle -- Whether to center the title. Defaults to true on iOS and false on Android.
  • flexibleSpace -- A widget stacked behind the toolbar and tab bar, useful for background effects.
  • toolbarHeight -- The height of the toolbar area (default is kToolbarHeight which is 56.0).

Custom AppBar with FlexibleSpace

The flexibleSpace property allows you to place a widget behind the AppBar content. This is often used for gradient backgrounds or images.

AppBar with Gradient Background

AppBar(
  title: const Text('Gradient AppBar'),
  centerTitle: true,
  foregroundColor: Colors.white,
  flexibleSpace: Container(
    decoration: const BoxDecoration(
      gradient: LinearGradient(
        colors: [Colors.purple, Colors.deepPurple],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
      ),
    ),
  ),
  elevation: 0,
)

PreferredSize Widget

Sometimes you need a custom app bar that does not use the standard AppBar widget. The PreferredSize widget tells the Scaffold how tall your custom app bar should be.

Custom AppBar with PreferredSize

Scaffold(
  appBar: PreferredSize(
    preferredSize: const Size.fromHeight(80),
    child: Container(
      decoration: const BoxDecoration(
        gradient: LinearGradient(
          colors: [Colors.blue, Colors.lightBlueAccent],
        ),
      ),
      child: const SafeArea(
        child: Center(
          child: Text(
            'Custom App Bar',
            style: TextStyle(
              color: Colors.white,
              fontSize: 22,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
      ),
    ),
  ),
  body: const Center(
    child: Text('Body Content'),
  ),
)
Tip: Use PreferredSize when you need full control over the app bar layout. Wrap your custom widget with SafeArea to avoid overlapping with the device status bar.

SliverAppBar Basics

The SliverAppBar is an advanced app bar that integrates with CustomScrollView to create scroll-based effects like collapsing, floating, and pinning. It is used inside a CustomScrollView instead of the Scaffold’s appBar property.

Basic SliverAppBar

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            expandedHeight: 200,
            floating: false,
            pinned: true,
            flexibleSpace: FlexibleSpaceBar(
              title: const Text('SliverAppBar Demo'),
              background: Image.network(
                'https://picsum.photos/800/400',
                fit: BoxFit.cover,
              ),
            ),
          ),
          SliverList(
            delegate: SliverChildBuilderDelegate(
              (context, index) => ListTile(
                title: Text('Item \${index + 1}'),
              ),
              childCount: 30,
            ),
          ),
        ],
      ),
    );
  }
}

Key SliverAppBar properties:

  • expandedHeight -- The height of the app bar when fully expanded.
  • floating -- If true, the app bar becomes visible as soon as the user scrolls up.
  • pinned -- If true, the app bar remains visible (collapsed) at the top when scrolling down.
  • snap -- If true (requires floating: true), the app bar snaps fully open or closed.
  • flexibleSpace -- Usually a FlexibleSpaceBar with a title and background image.
Warning: SliverAppBar must be used inside a CustomScrollView, not as the appBar property of Scaffold. If you place it in the Scaffold’s appBar slot, you will get a type error because SliverAppBar is not a PreferredSizeWidget.

Scaffold Drawer

The drawer property adds a navigation drawer that slides in from the left edge. When a drawer is provided, the AppBar automatically adds a hamburger menu icon as the leading widget.

Scaffold with Drawer

Scaffold(
  appBar: AppBar(
    title: const Text('Drawer Example'),
  ),
  drawer: Drawer(
    child: ListView(
      padding: EdgeInsets.zero,
      children: [
        const DrawerHeader(
          decoration: BoxDecoration(
            color: Colors.blue,
          ),
          child: Text(
            'App Menu',
            style: TextStyle(
              color: Colors.white,
              fontSize: 24,
            ),
          ),
        ),
        ListTile(
          leading: const Icon(Icons.home),
          title: const Text('Home'),
          onTap: () {
            Navigator.pop(context);
          },
        ),
        ListTile(
          leading: const Icon(Icons.settings),
          title: const Text('Settings'),
          onTap: () {
            Navigator.pop(context);
          },
        ),
      ],
    ),
  ),
  body: const Center(
    child: Text('Swipe from left or tap the menu icon'),
  ),
)

Scaffold Bottom Navigation Bar

The bottomNavigationBar property adds a navigation bar at the bottom of the screen, commonly used for switching between main sections of an app.

Bottom Navigation Bar Example

class MainScreen extends StatefulWidget {
  const MainScreen({super.key});

  @override
  State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  int _selectedIndex = 0;

  final List<Widget> _pages = const [
    Center(child: Text('Home Page')),
    Center(child: Text('Search Page')),
    Center(child: Text('Profile Page')),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Bottom Nav Example'),
      ),
      body: _pages[_selectedIndex],
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _selectedIndex,
        onTap: (index) {
          setState(() {
            _selectedIndex = index;
          });
        },
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.search),
            label: 'Search',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: 'Profile',
          ),
        ],
      ),
    );
  }
}

Scaffold Bottom Sheet

A bottomSheet is a persistent panel that stays visible at the bottom of the screen. For modal bottom sheets that slide up temporarily, use showModalBottomSheet() instead.

Persistent vs Modal Bottom Sheet

// Persistent bottom sheet (always visible)
Scaffold(
  appBar: AppBar(title: const Text('Bottom Sheet')),
  body: const Center(child: Text('Main Content')),
  bottomSheet: Container(
    height: 60,
    color: Colors.grey[200],
    child: const Center(
      child: Text('This is a persistent bottom sheet'),
    ),
  ),
)

// Modal bottom sheet (triggered by user action)
ElevatedButton(
  onPressed: () {
    showModalBottomSheet(
      context: context,
      builder: (context) {
        return Container(
          height: 200,
          padding: const EdgeInsets.all(16),
          child: const Column(
            children: [
              Text(
                'Modal Bottom Sheet',
                style: TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.bold,
                ),
              ),
              SizedBox(height: 16),
              Text('This slides up and can be dismissed.'),
            ],
          ),
        );
      },
    );
  },
  child: const Text('Show Modal Sheet'),
)

Practical Example: Complete App Shell

Let us combine everything into a realistic app shell that uses Scaffold with AppBar, Drawer, FAB, and BottomNavigationBar:

Complete App Shell

class AppShell extends StatefulWidget {
  const AppShell({super.key});

  @override
  State<AppShell> createState() => _AppShellState();
}

class _AppShellState extends State<AppShell> {
  int _currentIndex = 0;
  final List<String> _titles = ['Dashboard', 'Projects', 'Messages'];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(_titles[_currentIndex]),
        actions: [
          IconButton(
            icon: const Icon(Icons.notifications_outlined),
            onPressed: () {},
          ),
        ],
      ),
      drawer: Drawer(
        child: ListView(
          padding: EdgeInsets.zero,
          children: [
            const UserAccountsDrawerHeader(
              accountName: Text('John Doe'),
              accountEmail: Text('john@example.com'),
              currentAccountPicture: CircleAvatar(
                child: Text('J'),
              ),
            ),
            ListTile(
              leading: const Icon(Icons.dashboard),
              title: const Text('Dashboard'),
              onTap: () => _selectPage(0),
            ),
            ListTile(
              leading: const Icon(Icons.folder),
              title: const Text('Projects'),
              onTap: () => _selectPage(1),
            ),
            ListTile(
              leading: const Icon(Icons.message),
              title: const Text('Messages'),
              onTap: () => _selectPage(2),
            ),
            const Divider(),
            ListTile(
              leading: const Icon(Icons.settings),
              title: const Text('Settings'),
              onTap: () {
                Navigator.pop(context);
              },
            ),
          ],
        ),
      ),
      body: Center(
        child: Text(
          '\${_titles[_currentIndex]} Content',
          style: const TextStyle(fontSize: 24),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        child: const Icon(Icons.add),
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.dashboard),
            label: 'Dashboard',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.folder),
            label: 'Projects',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.message),
            label: 'Messages',
          ),
        ],
      ),
    );
  }

  void _selectPage(int index) {
    setState(() {
      _currentIndex = index;
    });
    Navigator.pop(context); // Close drawer
  }
}
Tip: Use UserAccountsDrawerHeader instead of a plain DrawerHeader when you want to display user profile information with an avatar, name, and email. It provides a polished look with minimal effort.

Practice Exercise

Build a Scaffold-based screen with the following features: (1) An AppBar with a gradient background using flexibleSpace, a centered title, and two action icons. (2) A Drawer with at least three navigation items. (3) A body that displays different content based on the selected drawer item. (4) A FloatingActionButton positioned at centerFloat. Challenge: Add a BottomNavigationBar with three tabs and sync it with the drawer navigation.