Skip to main content

Provide the theme

The theme role splits the theme between its provider and its template. A provider owns the look: the light and the dark theme of the app. The template of the role owns the theme mode that the user selects, the same with every provider. How settings work describes the mode, and Theme what the app has.

The two functions​

A provider generates two functions in lib/core/theme/app_theme.dart. Each takes the BuildContext of the root of the app and returns a ThemeData: createLightTheme one whose brightness is Brightness.light, and createDarkTheme one whose brightness is Brightness.dark. This file of a brick is all that a provider needs. It derives both themes from one color:

bricks/seed_theme/__brick__/lib/core/theme/app_theme.dart
import 'package:flutter/material.dart';

/// The colour that the light and the dark theme of the app derive their
/// colours from. Change it to give the app another look.
const Color seedColor = Colors.teal;

/// The light theme of the app.
///
/// The root of the app calls it each time it builds, so a change of this
/// file shows on a hot reload. [context] is the context of the root, above
/// its `MaterialApp`: it has neither the theme nor the localizations of the
/// app.
ThemeData createLightTheme(BuildContext context) => _themeOf(Brightness.light);

/// The dark theme of the app; see [createLightTheme].
ThemeData createDarkTheme(BuildContext context) => _themeOf(Brightness.dark);

/// The Material 3 theme of [brightness] in the colours of [seedColor]. What
/// both themes of the app share goes here, such as the shape of its
/// buttons.
ThemeData _themeOf(Brightness brightness) => ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: seedColor,
brightness: brightness,
),
);

The developer of the app edits this file for another look, so its comments say where each thing is changed.

A module whose brick has this file is a plain provider:

final class SeedThemeModule extends SmfModule {
/// Creates the module.
const SeedThemeModule();

/// The id of the module.
static const id = ModuleId('seed_theme');


ModuleDescriptor get descriptor => const ModuleDescriptor(
id: id,
description: 'Light and dark Material 3 themes from one seed colour',
kind: ModuleKinds.infrastructure,
providers: [RoleProvider.plain(themeRole)],
);


List<Contribution> contribute(ModuleContext context) => [
BrickContribution(seedThemeBundle),
];
}

A provider with a font​

The built-in material_theme is a plain provider too. Its file is longer: a palette, text styles and the themes of the components. Its themes are in a font that the app bundles. The brick has the files of the font and their license in assets/fonts/geist/, and the module declares them in the pubspec of the app, next to its brick and its note for coding agents:

  
List<Contribution> contribute(ModuleContext context) => [
BrickContribution(materialThemeBundle),
PubspecContribution.flutter(
assets: const [fontLicenseFile],
fonts: [
PubspecFont(fontFamily, [
for (final MapEntry(key: weight, value: file)
in fontFiles.entries)
PubspecFontAsset(file, weight: weight),
]),
],
),
AppEntryRole.agentSections.entry(
themeRole.description,
AgentNote(agentNote),
),
];

fontFiles maps each weight of the font to its file in the app, and fontLicenseFile is the license. With the files in the brick, the app gets no package for the font and loads none from the network. The license is an asset of the app, so that each copy of the app has it with the font. The note tells a coding agent where the colors and the font of the themes are, and what a change of the file must keep; see Notes for coding agents.

What the role does itself​

The template of the role does the rest, the same with every provider. It gives the root MaterialApp its theme, its darkTheme and its themeMode, keeps the theme mode that the user selects, saves it in the preferences and restores it, and gives the settings screen an entry that selects it. So a provider:

  • keeps no mode of its own and adds no entry for the mode;
  • sets none of the three arguments of the root, each of which takes one value. smf create stops with an error on a module that sets one of them in an app with the role;
  • requires what the role requires, the preferences role. So an app with the provider has a module that provides the preferences, and the registry of the tests of the provider has one too.

The context of the functions​

The context that the two functions get is the one of the root widget: below the root wrappers and above the root MaterialApp, so it has neither the theme nor the localizations of the app.

A provider whose look is fixed leaves the context alone, as material_theme does. A provider whose look depends on state of its own, such as a color that the user picks, puts an inherited widget with that state among the root wrappers and reads it from the context in the two functions. The root rebuilds in the new look when that widget notifies. For a setting of its look, such a provider contributes an entry of its own to the settings screen, as Add a setting shows.

Tests​

The tests of a provider select a mode with appThemeMode.choose(mode) and read it from appThemeMode.value, whichever module provides the role; see Theme.

The contract harness checks that a provider generates the symbols its role requires and follows the rules of the role. See Test a module.

SMF's CI checks the theme in running apps, with every provider of the role that SMF has: the root takes each mode that code chooses, the screens get a light theme in the light mode and a dark one in the dark mode, with the color schemes that the two functions return, and the next start has the mode that was saved. Contributing lists these tests.