Proposal: Add `themeBuilder` to MaterialApp to eliminate theme duplication
- Dominant language
- Dart
- Stars
- 179k
- Forks
- 31.1k
- PR merge metrics
- PR metrics pending
Description
### Use case
Flutter recommends using `ThemeData.from(colorScheme: ...)` together with `ColorScheme.fromSeed(..., brightness: brightness)`. With this pattern, the only thing that differs between light and dark themes is `Brightness` — almost of settings not related to color (typography, shape, component themes, some of `ThemeExtension`s, etc.) are identical.
However, `MaterialApp` only accepts fully-specified `ThemeData` objects for each of `theme`, `darkTheme`, `highContrastTheme`, and `highContrastDarkTheme`. The only way to avoid duplication is to define a helper function on the user side:
```dart
MaterialApp(
theme: _buildTheme(Brightness.light, false),
darkTheme: _buildTheme(Brightness.dark, false),
highContrastTheme: _buildTheme(Brightness.light, true),
highContrastDarkTheme: _buildTheme(Brightness.dark, true),
)
ThemeData _buildTheme(Brightness brightness, bool highContrast) =>
ThemeData.from(
colorScheme: ColorScheme.fromSeed(
seedColor: kSeedColor,
brightness: brightness,
),
).copyWith(
cardTheme: ...,
inputDecorationTheme: ...,
extensions: [AppColors.fromBrightness(brightness, highContrast)],
);
```
The framework offers no first-class support for this pattern, so developers unfamiliar with it naturally write separate `ThemeData` objects for each property, leading to drift over time.
### Proposal
Add a `themeBuilder` property to `MaterialApp` with the following signature:
```dart
themeBuilder: (Brightness brightness, bool highContrast) => ThemeData
```
Example usage:
```dart
MaterialApp(
themeBuilder: (brightness, highContrast) => ThemeData.from(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: brightness,
),
).copyWith(
cardTheme: const CardThemeData(elevation: 2),
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
),
extensions: [
AppColors.fromBrightness(
brightness,
highContrast,
),
],
),
themeMode: ThemeMode.system,
)
```
When `themeBuilder` is provided, `MaterialApp` calls it internally with the resolved `Brightness` and `highContrast`, replacing the need for `theme`, `darkTheme`, `highContrastTheme`, and `highContrastDarkTheme`. If dark theme is not needed, the brightness argument can simply be ignored.`themeBuilder` and those four properties would be mutually exclusive (assertion error if both are set). `themeMode` continues to work as-is.
Additionally, since there is no need to instantiate a separate `ThemeData` for each theme variant upfront, there may be a marginal performance improvement.
Contributor guide
Assessment
This issue has not been assessed yet.