1. Material 3 Components
Scaffold is the standard page skeleton in Flutter — it provides the
structure (app bar, body, floating action button, drawer) that almost every screen
needs, and handles a surprising amount of platform-correct behavior for free.
class ProductListScreen extends StatelessWidget {
const ProductListScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Products')),
body: ListView(
children: [
Card(
child: ListTile(
leading: const Icon(Icons.shopping_bag),
title: const Text('Wireless Mouse'),
subtitle: const Text('₹1,299'),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
);
}
}
ListTile in particular is worth knowing well — it's the standard
three-slot row (leading icon, title/subtitle, trailing widget) that shows up in
settings screens, contact lists, and menus across almost every real Flutter app.
2. Light/Dark Theming with ColorScheme.fromSeed
Rather than picking a dozen colors by hand, Material 3's
ColorScheme.fromSeed generates an entire, accessible, harmonious color
scheme — primary, secondary, surface, error, and their "on" (text/icon) pairs — from
a single brand color.
MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF6750A4)),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6750A4),
brightness: Brightness.dark,
),
useMaterial3: true,
),
themeMode: ThemeMode.system, // follows the device's setting automatically
home: const ProductListScreen(),
)
Widgets that read colors from Theme.of(context).colorScheme (which
every Material widget does by default) automatically pick up whichever theme is
active — there's no per-widget dark-mode branching to write.
3. MediaQuery, LayoutBuilder & Adaptive Layouts
MediaQuery reports facts about the whole screen (size, padding, text
scale); LayoutBuilder reports the constraints of just the widget's own
parent — the right tool depends on whether "adapt to" means the device or a
specific part of the layout.
final screenWidth = MediaQuery.of(context).size.width;
final isTablet = screenWidth >= 600;
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= 600) {
return const GridView2Column();
}
return const ListViewSingleColumn();
},
)
LayoutBuilder is usually the better choice inside a nested layout — a
widget that's only ever shown in a 300px-wide sidebar should adapt to that 300px,
not to the phone's full 400px screen width that MediaQuery would
report.
4. Hands-on Exercise
Rebuild the product list with Material 3 and responsive layout
Take Week 2's raw-widget screen and give it real theming and adaptive behavior.
Requirements:
- Rebuild the screen with
Scaffold,AppBar, and a list ofCard/ListTilerows for at least 6 items. - Generate a full color scheme with
ColorScheme.fromSeedfrom a color of your choice, wired to boththemeanddarkThemewiththemeMode: ThemeMode.system. - Confirm the screen looks correct in both light and dark mode by toggling your device/simulator's system theme, with no manual color overrides needed anywhere.
- Using
LayoutBuilder, show a single-column list under 600px width and a 2-column grid at 600px and above. - Test the responsive behavior by resizing a browser window (Flutter web) or a resizable desktop/emulator window, not just guessing from the code.
If your dark theme looks correct except for one or two colors that stay stubbornly light, check whether those specific widgets have a hardcoded Color(0xFF...) instead of reading from Theme.of(context).colorScheme — a hardcoded color never responds to theme changes, which is exactly why Material widgets are built to read theme colors instead.
5. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
What does ColorScheme.fromSeed actually generate, beyond just one color?
What does ColorScheme.fromSeed actually generate, beyond just one color?
A complete, harmonious set of colors derived from the seed — primary, secondary, tertiary, surface, error, and each of their paired 'on' colors for text/icons — all designed to meet accessibility contrast guidelines together, not just one accent color repeated everywhere.
Q2
Why does a widget reading Theme.of(context).colorScheme automatically support dark mode with no extra code, while a widget with a hardcoded color does not?
Why does a widget reading Theme.of(context).colorScheme automatically support dark mode with no extra code, while a widget with a hardcoded color does not?
Theme.of(context) resolves to whichever theme (light or dark) is currently active based on themeMode and the device setting — a widget reading colors through it always gets the correct theme's colors. A hardcoded Color(...) is a fixed value with no connection to the active theme at all, so it never changes regardless of mode.
Q3
When would LayoutBuilder give a more correct answer than MediaQuery for "should this switch to a compact layout"?
When would LayoutBuilder give a more correct answer than MediaQuery for "should this switch to a compact layout"?
When the widget in question doesn't actually occupy the full screen — inside a sidebar, a dialog, a split-view pane — MediaQuery would report the full device width, which has nothing to do with the widget's actual available space. LayoutBuilder reports the real constraints handed down from that widget's immediate parent, which is what its layout decision should actually be based on.
Q4
Why is a 600px width a reasonable, common threshold for switching between a phone-style single column and a tablet-style multi-column layout?
Why is a 600px width a reasonable, common threshold for switching between a phone-style single column and a tablet-style multi-column layout?
600px roughly separates typical phone screen widths from tablet and larger form factors in Material Design's own breakpoint guidance — it's not a hard technical requirement, just a widely-adopted convention that gives a consistent, predictable point for a layout to reflow, which is why it shows up as a common threshold across many real Flutter apps rather than each app inventing its own arbitrary number.