flutter / flutter/flutter

Proposal: Add `themeBuilder` to MaterialApp to eliminate theme duplication

Open
#185,189 2 comments 1 reaction 0 assignees View on GitHub
c: proposal p: material_ui P3 package team-design triaged-design
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.