Themes & Styling
Sprinkling colors and font sizes directly into every single widget works for a demo, but it falls
apart fast in a real app — change the brand color, and suddenly you're hunting through dozens of
files. ThemeData is Flutter's answer: a single, central place to define your app's
colors, typography, and component styling once, which every widget in the tree can then
read from consistently.
ThemeData and MaterialApp
A ThemeData object is handed to MaterialApp through its theme:
property. Once set there, it's automatically available to every widget beneath it in the tree —
you never have to pass it down manually.
MaterialApp(
title: 'My App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const HomeScreen(),
)
A Full Worked ThemeData Example
Material 3's modern approach doesn't ask you to pick a dozen individual colors by hand — instead,
ColorScheme.fromSeed() takes a single seed color and algorithmically
derives an entire harmonious palette from it (primary, secondary, surface, error, and their
"on-" counterparts for text/icons that sit on top of them). You can then layer a custom
textTheme on top for typography.
final appTheme = ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.light,
),
textTheme: const TextTheme(
headlineSmall: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
bodyMedium: TextStyle(fontSize: 15, height: 1.4),
bodySmall: TextStyle(fontSize: 12, color: Colors.grey),
),
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
),
);
AppBar, ElevatedButton, Card, TextField — has its own *Theme class (AppBarTheme, ElevatedButtonTheme, and so on) that can be set inside ThemeData to style every instance of that widget app-wide at once.Reading the Theme with Theme.of(context)
Once a theme is set on MaterialApp, any widget in the tree can read it back with
Theme.of(context) — the same .of(context) pattern covered in
The Widget Tree & BuildContext. Under the hood,
it's walking up the widget tree from the current BuildContext to find the nearest
Theme (itself backed by an InheritedWidget) and returning its
ThemeData.
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Text(
'Welcome back',
style: theme.textTheme.headlineSmall?.copyWith(
color: theme.colorScheme.primary,
),
);
}
Dark Mode Support
Supporting dark mode properly is just a second ThemeData plus one property. Define a
darkTheme: alongside theme: on MaterialApp, then control
which one is active with themeMode:.
MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.light,
),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.dark,
),
useMaterial3: true,
),
// Follows the device's system setting automatically
themeMode: ThemeMode.system,
home: const HomeScreen(),
)
ThemeMode.system is usually the right default — the app switches automatically
whenever the user toggles dark mode at the OS level. Use ThemeMode.light or
ThemeMode.dark instead when you want to force one appearance regardless of the
device's setting, for example to let the user override it manually from an in-app settings screen
(a choice you'd typically persist with shared_preferences
so it survives a restart).
One-Off Styling on Individual Widgets
Theming shines for anything that should look consistent across the whole app, but not every style
decision belongs in ThemeData. When a single widget needs a genuine one-off
override — a uniquely colored warning banner, one specially styled promotional button — it's
perfectly fine, and often clearer, to style that widget directly instead of bending the global
theme around a single exception.
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'Your session will expire soon',
style: TextStyle(color: Colors.deepOrange, fontWeight: FontWeight.w600),
),
)
ThemeData. If it's a genuine one-off exception, style that single widget directly.Custom Typography with Google Fonts
ThemeData's default typography relies on the system font, which looks fine but generic.
The google_fonts package is the common way to bring in custom typography from Google's
font library without manually downloading font files and registering them in
pubspec.yaml — a heavier, manual approach briefly mentioned in
Images & Assets. google_fonts fetches and
caches the font for you at runtime, and slots directly into a TextTheme.
ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
textTheme: GoogleFonts.poppinsTextTheme(),
)