This project implements a solid, scalable theming architecture for Flutter based on:
- 🎨 Design tokens in JSON (one file per brand)
- ⚙️ Build‑time code generation (no runtime parsing)
- 🧩 Brand theme pairs (light + dark always applied together)
- 🎛️ Cubit runtime control (switch brand + theme mode)
The goal is to keep UI code free from hex colors and “mystery” Material mappings, while ensuring the designer’s token changes propagate safely and consistently.
For applications that do not require multi-brand support, a simplified version of this architecture is available.
👉 Single-brand theme token approach:
https://github.com/Abdo-Nabil/single-theme-token-approach
- ❌ No hardcoded hex colors in widgets
- ❌ No duplicated colors across the app
- ❌ No unclear “which ColorScheme field does this widget use?”
- ✅ A single source of truth per brand (
blue.json,green.json,red.json) - ✅ Compile‑time, type‑safe access (
context.colors.pageBg, etc.) - ✅ Safe runtime switching via Cubit (brand + theme mode)
lib/
main.dart
home_page.dart
theme/
design_tokens/ # 🎨 Designer-owned (one JSON per brand)
blue.json
green.json
red.json
theme_generator.dart # ⚙️ Build-time generator (run manually / CI)
generated_colors/ # 🤖 AUTO-GENERATED (do not edit)
app_colors.g.dart # AppColors ThemeExtension (generated contract)
blue_colors.g.dart # brand values (light/dark)
green_colors.g.dart
red_colors.g.dart
color_registry.g.dart # brand+brightness resolver
core/ # 🧱 Stable infrastructure
app_brand.dart # enum of brands (manual)
theme_pair.dart # light/dark pair abstraction (manual)
context_colors.dart # BuildContext → AppColors (manual)
cubit/ # 🎛️ Runtime state control
theme_state.dart
theme_cubit.dart
app_theme.dart # 🎯 ThemeData builder (manual)
Important: this project uses multiple token files (one per brand) — not a single
colors.json.
Each brand has its own token file:
lib/theme/design_tokens/blue.jsonlib/theme/design_tokens/green.jsonlib/theme/design_tokens/red.json
Each file uses the same schema:
{
"themeName": "blue",
"tokens": {
"pageBg": { "light": "#F5F9FF", "dark": "#0D1117" },
"pageFg": { "light": "#121212", "dark": "#EDEDED" },
"cardBg": { "light": "#FFFFFF", "dark": "#161B22" },
"buttonPrimaryBg": { "light": "#0065FF", "dark": "#4A8DFF" },
"buttonPrimaryFg": { "light": "#FFFFFF", "dark": "#000000" },
"statusSuccess": { "light": "#4CAF50", "dark": "#81C784" },
"statusWarning": { "light": "#FFC107", "dark": "#FFD54F" },
"statusError": { "light": "#F44336", "dark": "#FF8A80" }
}
}- All brands must have identical token keys
- Every token must define both light and dark
- Colors must be #RRGGBB
- Token names must be valid Dart identifiers
The generator reads all *.json files under:
lib/theme/design_tokens/
Run from the project root:
dart lib/theme/theme_generator.dartThis produces:
lib/theme/generated_colors/
app_colors.g.dart
<brand>_colors.g.dart
color_registry.g.dart
app_colors.g.dart: definesAppColors extends ThemeExtension<AppColors>blue_colors.g.dart,green_colors.g.dart,red_colors.g.dart: defineconst AppColors <brand>LightColors/<brand>DarkColorscolor_registry.g.dart: maps(AppBrand, isDark)→AppColors
Themes are always applied as a pair:
ThemeDatafor lightThemeDatafor dark
AppTheme.byBrand(brand) returns a ThemePair:
final pair = AppTheme.byBrand(AppBrand.blue);
MaterialApp(
theme: pair.light,
darkTheme: pair.dark,
themeMode: ThemeMode.system,
);This ensures light/dark are never mixed across brands.
The Cubit controls selection, not definition:
- active brand (
AppBrand) - theme mode (
ThemeMode.system / light / dark)
class ThemeState {
final AppBrand brand;
final ThemeMode mode;
}context.read<ThemeCubit>().setBrand(AppBrand.green);
context.read<ThemeCubit>().setThemeMode(ThemeMode.dark);MaterialApp rebuilds based on Cubit state (brand + mode).
Widgets only consume tokens via BuildContext:
final c = context.colors;
Text('Hello', style: TextStyle(color: c.pageFg));No widget:
- reads JSON
- checks brightness
- branches by brand
- hardcodes hex values
- Add a new file:
lib/theme/design_tokens/purple.json
- Add the enum value:
enum AppBrand { blue, green, red, purple }- Run generator:
dart lib/theme/theme_generator.dart- Switch at runtime:
context.read<ThemeCubit>().setBrand(AppBrand.purple);- Delete the token file:
lib/theme/design_tokens/red.json
-
Remove from
AppBrand -
Run generator:
dart lib/theme/theme_generator.dart- Fix any compile errors where the removed brand was referenced
- ✅ Single source of truth per brand (JSON)
- ✅ Build‑time validation (generator fails fast)
- ✅ Compile‑time safety (typed token access)
- ✅ No runtime parsing overhead
- ✅ Atomic brand switching (light/dark pairs)
- ✅ Clean runtime orchestration (Cubit)
This repository implements a multi‑brand theming system where designers update brand token JSON files, developers run a build‑time generator, and the app uses typed tokens via context.colors. Runtime theme and brand switching is handled cleanly through Cubit, without leaking theme logic into widgets.