Why themes matter (and what they control)
In Flutter, styling can be applied at two levels: globally (with a theme) and locally (per widget). A global theme helps you keep colors, typography, and component styles consistent across screens without repeating the same TextStyle and color values everywhere. Local styling is still useful for one-off tweaks, but the goal is to let the theme do most of the work.
The main entry point for app-wide styling is ThemeData, usually provided to MaterialApp via the theme property. Modern Flutter styling is centered around ColorScheme (a structured set of semantic colors like primary, surface, error) and TextTheme (a set of named text styles like titleLarge, bodyMedium).
Step 1: Define a consistent ThemeData
Create a theme file
Start by creating a dedicated file for your theme so it stays reusable and easy to maintain.
- Create
lib/theme/app_theme.dart - Define a light theme (and optionally a dark theme)
import 'package:flutter/material.dart';
class AppTheme {
static const Color seedColor = Color(0xFF4F46E5); // Indigo-ish
static ThemeData light() {
final colorScheme = ColorScheme.fromSeed(
seedColor: seedColor,
brightness: Brightness.light,
);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
scaffoldBackgroundColor: colorScheme.surface,
textTheme: _textTheme(colorScheme),
appBarTheme: AppBarTheme(
backgroundColor: colorScheme.surface,
foregroundColor: colorScheme.onSurface,
elevation: 0,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
cardTheme: CardTheme(
elevation: 0,
color: colorScheme.surfaceContainerHighest,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
);
}
static TextTheme _textTheme(ColorScheme colorScheme) {
return TextTheme(
titleLarge: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
bodyMedium: TextStyle(
fontSize: 16,
height: 1.4,
color: colorScheme.onSurface,
),
labelLarge: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onPrimary,
),
);
}
}Apply the theme in MaterialApp
In your app entry widget, pass the theme to MaterialApp. This makes the theme available through Theme.of(context) and used automatically by many Material widgets.
import 'package:flutter/material.dart';
import 'theme/app_theme.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: AppTheme.light(),
home: const HomeScreen(),
);
}
}Use semantic colors instead of hard-coded colors
Prefer Theme.of(context).colorScheme over raw hex colors inside widgets. This keeps your UI adaptable (for example, if you later add a dark theme).
- Listen to the audio with the screen off.
- Earn a certificate upon completion.
- Over 5000 courses for you to explore!
Download the app
final cs = Theme.of(context).colorScheme;
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: cs.primaryContainer,
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Welcome',
style: TextStyle(color: cs.onPrimaryContainer),
),
);Step 2: Apply TextStyle the right way (TextTheme first)
Use TextTheme for consistent typography
Instead of styling every Text widget manually, pull styles from the theme. This gives you consistent font sizes and weights across the app.
Text(
'Profile',
style: Theme.of(context).textTheme.titleLarge,
);
Text(
'Edit your details below.',
style: Theme.of(context).textTheme.bodyMedium,
);Override only what you need with copyWith
If a specific widget needs a small variation (like a different color), keep the base style from the theme and override only the changed properties.
final tt = Theme.of(context).textTheme;
final cs = Theme.of(context).colorScheme;
Text(
'Warning: Unsaved changes',
style: tt.bodyMedium?.copyWith(
color: cs.error,
fontWeight: FontWeight.w600,
),
);Widget-level styling vs. theme-level styling
Use theme-level styling for patterns you want everywhere (buttons, inputs, cards). Use widget-level styling for special cases. A good rule: if you copy/paste the same style twice, it probably belongs in the theme or a reusable component.
Step 3: Create reusable UI components that respect the theme
Build a reusable primary button
Even with ElevatedButtonThemeData, you may want a reusable widget to standardize spacing, loading states, and icons. The key is to rely on the theme for colors and typography.
import 'package:flutter/material.dart';
class PrimaryButton extends StatelessWidget {
final String label;
final VoidCallback? onPressed;
final bool isLoading;
const PrimaryButton({
super.key,
required this.label,
required this.onPressed,
this.isLoading = false,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: isLoading ? null : onPressed,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: isLoading
? const SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(label),
),
),
);
}
}Create a themed info card
This component uses CardTheme and the app’s ColorScheme so it automatically matches your design.
import 'package:flutter/material.dart';
class InfoCard extends StatelessWidget {
final String title;
final String subtitle;
final IconData icon;
const InfoCard({
super.key,
required this.title,
required this.subtitle,
required this.icon,
});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final tt = Theme.of(context).textTheme;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: cs.primaryContainer,
borderRadius: BorderRadius.circular(14),
),
child: Icon(icon, color: cs.onPrimaryContainer),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: tt.titleLarge?.copyWith(fontSize: 18)),
const SizedBox(height: 4),
Text(subtitle, style: tt.bodyMedium),
],
),
),
],
),
),
);
}
}Practical usage example
Column(
children: [
InfoCard(
title: 'Storage',
subtitle: '2.1 GB used of 5 GB',
icon: Icons.cloud,
),
const SizedBox(height: 12),
PrimaryButton(
label: 'Upgrade',
onPressed: () {},
),
],
);Step 4: Load image assets (and configure pubspec.yaml)
Organize your assets folder
Create a predictable structure so assets stay manageable as your app grows.
assets/images/for PNG/JPG/WebPassets/icons/for icon images (if not using icon fonts)assets/illustrations/for larger visuals
Configure pubspec.yaml for assets
Flutter only bundles assets that are declared in pubspec.yaml. Indentation matters because YAML is whitespace-sensitive.
flutter:
uses-material-design: true
assets:
- assets/images/
- assets/illustrations/With the folder approach above, any file placed inside those directories will be bundled (for example assets/images/logo.png).
Display an asset image
Image.asset(
'assets/images/logo.png',
width: 120,
fit: BoxFit.contain,
);Use assets in a decoration
For backgrounds or cards, you can use DecorationImage.
Container(
height: 180,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
image: const DecorationImage(
image: AssetImage('assets/illustrations/banner.jpg'),
fit: BoxFit.cover,
),
),
);Step 5: Add and use custom fonts
Place font files in your project
Create a folder like assets/fonts/ and add your font files (commonly .ttf or .otf). Many font families come with multiple weights (Regular, Medium, Bold). Keep the filenames clear.
Configure pubspec.yaml for fonts
Declare a font family and list each font file with its weight. The family name is what you’ll reference in code.
flutter:
uses-material-design: true
fonts:
- family: Inter
fonts:
- asset: assets/fonts/Inter-Regular.ttf
weight: 400
- asset: assets/fonts/Inter-Medium.ttf
weight: 500
- asset: assets/fonts/Inter-Bold.ttf
weight: 700Apply the font globally via ThemeData
You can set fontFamily on ThemeData so all text uses it by default.
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
fontFamily: 'Inter',
textTheme: _textTheme(colorScheme),
);Apply a custom font to a specific widget (optional)
If you don’t want a global font, you can apply it per widget using TextStyle.
Text(
'Special Heading',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 24,
fontWeight: FontWeight.w700,
),
);Troubleshooting: common asset and font loading mistakes
1) “Unable to load asset” at runtime
- Check the path string exactly matches the file location and filename (case-sensitive on many systems).
- Confirm the asset is declared in
pubspec.yamlunderflutter:and that indentation is correct. - If you added the asset while the app was running, do a full restart (hot reload may not pick up new assets).
2) Assets declared but still not found
- Make sure you used a trailing slash when declaring a directory (for example
assets/images/). - Verify the asset is inside the declared directory (not a sibling folder).
- Run
flutter pub getafter editingpubspec.yaml.
3) YAML indentation errors
- YAML uses spaces, not tabs. A single wrong indentation level can break asset discovery.
- Ensure
assets:andfonts:are aligned underflutter:. - Each list item must be indented consistently (for example two spaces under
assets:).
4) Custom font doesn’t apply
- Confirm the
familyname inpubspec.yamlmatches exactly what you use infontFamily. - Ensure the font files exist at the specified paths.
- If only some weights work, verify you declared the correct
weightvalues and that the files are the intended weights. - Do a full restart after adding fonts.
5) Text looks different on iOS vs Android
- Without a custom font, platforms may render default fonts differently. Setting a custom
fontFamilyin the theme helps unify appearance. - Check that you’re using
TextThemestyles consistently rather than mixing many ad-hocTextStyledefinitions.
Practical checklist: make styling consistent across your app
- Define
ColorSchemeonce and reference it viaTheme.of(context).colorScheme. - Define a small, intentional
TextThemeand use it everywhere. - Theme common components (buttons, inputs, cards) in
ThemeDatato reduce repetition. - Create reusable widgets for repeated patterns (cards, list rows, buttons) that rely on the theme.
- Declare assets and fonts in
pubspec.yaml, then full restart after changes.