In Flutter, how do you style widgets from the app theme instead of hardcoding colors and text styles?
answer
- one ThemeData at the app root
- MaterialApp's theme parameter
- Theme.of(context) walks up the tree
- colorScheme roles like primary and onPrimary
- textTheme styles like bodyMedium
basics
~10 sDefine one ThemeData, usually with a ColorScheme and a TextTheme, pass it to MaterialApp's theme, then read Theme.of(context).colorScheme and .textTheme in widgets instead of writing Colors.blue or a literal TextStyle.
solid answer
~40 sI put a single `ThemeData` on `MaterialApp.theme`, built from a `ColorScheme` (for example `ColorScheme.fromSeed`) and a `TextTheme`. Material widgets such as `AppBar`, `FilledButton` and `Card` already read their defaults from it. In my own widgets I read `Theme.of(context).colorScheme.primary` or `Theme.of(context).textTheme.titleLarge` rather than `Colors.indigo` or a literal `TextStyle(fontSize: 22)`. I pair every background role with its `on*` role, so text on `primary` uses `onPrimary`. When a style needs a tweak I use `copyWith` on the theme style, so the brand change or dark theme still flows through. The payoff is one place to change the brand and a dark theme that works without touching screens.
code
dart · 31 linesimport 'package:flutter/material.dart';
void main() => runApp(const LoyaltyApp());
class LoyaltyApp extends StatelessWidget {
const LoyaltyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF00696B)),
),
home: Builder(
builder: (BuildContext context) {
final ColorScheme scheme = Theme.of(context).colorScheme;
return Scaffold(
body: Center(
child: Text(
'Welcome back',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: scheme.primary,
),
),
),
);
},
),
);
}
}go deeper
Recall the chain: ThemeData on MaterialApp.theme, then Theme.of(context).colorScheme and .textTheme in build. Name a few roles such as primary, onPrimary, surface and bodyMedium.
Explain why on-roles exist, why copyWith on a theme style beats a new TextStyle, and why Theme.of must be called from a context below MaterialApp.
Show how you keep a codebase free of literal colors: lint rules or review checks, a theme file per app, and clear exceptions for brand assets.
Frame theming as a contract between design and code: roles as the shared vocabulary, so redesigns and new modes land by editing ThemeData rather than screens.
## What a theme is in Flutter A **theme** in Flutter's Material library is a `ThemeData` object: an immutable bundle of colors, typography, shapes and per-component defaults. You hand it to `MaterialApp` through the `theme` parameter, and `MaterialApp` places it in the widget tree so that every descendant can look it up. If you pass nothing, `MaterialApp` builds a default `ThemeData()` for you, which in current Flutter is a Material 3 theme (`useMaterial3` has defaulted to `true` since Flutter 3.16). The two properties that almost every app sets are: - **`colorScheme`** — a `ColorScheme` holding the Material 3 color roles: `primary`, `onPrimary`, `primaryContainer`, `secondary`, `tertiary`, `error`, `surface`, `onSurface`, the `surfaceContainer*` family, `outline` and more. - **`textTheme`** — a `TextTheme` holding fifteen named styles in five groups: `display`, `headline`, `title`, `body` and `label`, each in `Large`, `Medium` and `Small` (for example `bodyMedium`, `titleLarge`, `labelSmall`). ## Reading the theme in a widget Inside `build`, call `Theme.of(context)`. It returns the `ThemeData` of the nearest `Theme` ancestor, which is normally the one `MaterialApp` installed. From there you read roles, not raw values: ```dart import 'package:flutter/material.dart'; class PointsBanner extends StatelessWidget { const PointsBanner({super.key, required this.points}); final int points; @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); return ColoredBox( color: theme.colorScheme.primaryContainer, child: Padding( padding: const EdgeInsets.all(16), child: Text( '$points points', style: theme.textTheme.titleLarge?.copyWith( color: theme.colorScheme.onPrimaryContainer, ), ), ), ); } } ``` Two conventions carry most of the value: 1. **Pair every surface role with its `on*` role.** Text or icons drawn on `primary` use `onPrimary`; content on `surface` uses `onSurface`. The scheme is generated so that these pairs are legible in both light and dark. 2. **Adjust a theme style with `copyWith`, never replace it.** `textTheme.titleLarge?.copyWith(color: ...)` keeps the size, weight and font the theme chose; a fresh `TextStyle(fontSize: 22)` silently opts out of any later typography change. The `TextTheme` fields are nullable, hence the `?.`. ## What the Material widgets already do You rarely need to pass colors to Material components at all. `AppBar`, `FilledButton`, `NavigationBar`, `Card`, `Checkbox` and the rest compute their defaults from `colorScheme` and `textTheme`. That means a theme change restyles them automatically, and your own widgets stay consistent with them only if they also read the theme. | Approach | Brand change | Dark theme | Consistency with Material widgets | |---|---|---|---| | `Colors.indigo`, literal `TextStyle` | Edit every file | Breaks or needs `if` checks | Drifts | | `Theme.of(context).colorScheme` / `.textTheme` | Edit the `ThemeData` only | Follows the active theme | Matches | ## Alpha and the `Color` API in current Flutter When a design asks for a translucent version of a role color, use **`Color.withValues(alpha: 0.12)`**. Since Flutter 3.27 a `Color` stores its components as floating-point values, and `withOpacity` and `.opacity` are deprecated in favour of `withValues` and `.a`, because the old methods quantised alpha to 8 bits. The old methods still exist but produce deprecation warnings. ## Common mistakes - Hardcoding `Colors.white` for text on a colored surface, which becomes invisible in a light or dark variant. - Creating a new `ThemeData()` inside a screen to get "a theme", which throws away every app-level setting instead of inheriting it. - Assuming a `TextTheme` style is non-null and calling methods on it without `?.` or a fallback. - Styling a custom widget from `Colors.grey` because "it is only a divider", which then ignores `colorScheme.outlineVariant` and looks wrong in dark mode. ## Theme.of versus the shortcuts `Theme.of(context)` is the general entry point. The Material library also offers shortcuts that read one part of it: `ColorScheme.of(context)` returns `Theme.of(context).colorScheme`, and `TextTheme.of(context)` returns `Theme.of(context).textTheme`. They behave identically; use whichever reads better, but be consistent within a codebase. What you should not do is cache a `ThemeData` in a field or a global variable: the active theme changes when the user switches to dark mode or when a subtree wraps itself in a local `Theme`, and only a lookup through the current `context` sees the right one. ## Where this leads Once widgets read roles from `Theme.of(context)`, you can add a `darkTheme`, generate schemes from a seed color, add brand tokens through `ThemeExtension`, and set per-component defaults through component themes such as `FilledButtonThemeData`, all without editing screens. That is the whole reason interviewers ask this question: they want to hear that styling decisions live in one `ThemeData`, and widgets ask for meaning (primary, error, title) rather than for a specific hex value.
- In Flutter, why does Theme.of(context) inside MaterialApp's own build method not see the theme you just passed to it?`Theme.of` looks for the nearest `Theme` ancestor of the given context. The context of the widget that builds `MaterialApp` sits above the `Theme` that `MaterialApp` inserts, so the lookup finds nothing app-specific and returns the fallback theme. Read the theme from a descendant context: a child widget's `build`, or a `Builder` placed under `MaterialApp`.
- In Flutter 3.27 and later, what replaces Color.withOpacity, and why was it deprecated?`Color.withValues(alpha: 0.5)` replaces `withOpacity(0.5)`, and `.a` replaces `.opacity`. Since 3.27 `Color` stores components as floats to support wide-gamut color, so the old 8-bit opacity methods lose precision. They were deprecated rather than removed, and still work with a warning.
- In Flutter's Material library, when is it acceptable to write a literal color in a widget?When the color is genuinely not part of the design language: a third-party brand logo's fixed color, a chart palette that the design keeps separate, or a debug overlay. Even then, a named constant or a `ThemeExtension` field beats an inline literal, because a reviewer can see it was a deliberate exception.
saying these in an interview costs you the question
- Writing Colors.white for text because the button background is dark
- Creating a fresh ThemeData() in a screen to style one widget
- Replacing a TextTheme style with a new TextStyle instead of copyWith
- Using withOpacity as the current way to make a role color translucent
- Believing Material widgets ignore ThemeData unless every color is passed explicitly