Theming

1. Theme Elements

  • Color Palette
    • Primary, Secondary, Background, Surface, Error, Success, Warning, Info
    • Shades (light/dark variations)
  • Typography
    • Font Family, Font Sizes, Font Weights, Line Heights, Letter Spacing

2. Theme Injection Levels

Theme values should be inherited via context BuildContext, and can be injected at multiple scopes.

  • L1: Root-level Theme (Global Base)
    • Default theme provided by the component library.
    • Can be initialized from the application-level theme model, but transformed into the component’s internal theme model.
  • L2: Parent-level Theme Override
    • Any ancestor container (e.g., Section, Page, Layout) can override a subset of theme values.
    • Example: A dark section inside a light page.
  • L3: Component-level Theme Override
    • Individual components can directly override theme values via props/config.

3. Priority of Resolution

Theme values are resolved using a fallback chain:

L3 (Component-level override)  
   ⬇  
L2 (Parent-level override)  
   ⬇  
L1 (Root/Application-level base)  
   ⬇  
Default (library-defined defaults)

This ensures components are customizable at any granularity without breaking global consistency.


4. Component Theme Model vs App Theme Model

  • The Application-level theme model (e.g., Flutter ThemeData) may contain global brand-wide settings.
  • Before injection, this is mapped/transformed into the Component-level theme model, which is:
    • Simplified → only includes what components need (colors + typography).
    • Decoupled → prevents breaking changes if the app’s theme evolves differently.

Example

Application Level

class MyApp extends StatelessWidget {
  const MyApp({super.key});
 
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<RootBloc, RootState>(
      builder: (context, state) {
        return MaterialApp.router(
          theme: Theme.of(context).copyWith(
            extensions: [
              state.theme,
              state.config,
              MyComponentTheme(
                // injection level-1
                color: state.theme.primary,
                errorColor: state.theme.basic.red,
              ),
            ],
          ),
        );
      },
    );
  }
}
 
class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});
 
  @override
  Widget build(BuildContext context) {
    return MyComponentThemeData(
      // injection level-2
      data: MyComponentTheme(
        color: context.theme.primary,
        errorColor: context.theme.basic.red,
      ),
      child: Column(
        // n-level of parent-hierarchy of widget
        children: [
          Container(
            // n-level of parent-hierarchy of widget
            child: MyComponent(
              theme: MyComponentTheme(
                // injection level-3
                color: context.theme.primary,
                errorColor: context.theme.basic.red,
              ),
            ),
          )
        ],
      ),
    );
  }
}

Component Level

class MyComponentTheme extends ThemeExtension<MyComponentTheme> {
  final Color color;
  final Color errorColor;
 
  const MyComponentTheme({
    required this.color,
    required this.errorColor,
  });
 
  // set default values here
  static get empty => MyComponentTheme(
        color: Colors.blue,
        errorColor: Colors.red,
      );
}
 
class MyComponentThemeData extends StatelessWidget {
  final MyComponentTheme data;
  final Widget child;
 
  const MyComponentThemeData({
    super.key,
    required this.data,
    required this.child,
  });
 
  @override
  Widget build(BuildContext context) {
    return child;
  }
}
 
class MyComponent extends StatelessWidget {
  final MyComponentTheme? theme;
  // other properties
 
  const MyComponent({super.key, this.theme});
 
  @override
  Widget build(BuildContext context) {
    return MyComponentThemeData(
      // sequence is important here
      //
      // current theme-data >
      //    very immediate parent theme-data >
      //      root-level theme-data >
      //          default theme-data
      data: theme ??
          context.findAncestorWidgetOfExactType<MyComponentThemeData>()?.data ??
          Theme.of(context).extension<MyComponentTheme>() ??
          MyComponentTheme.empty,
      child: const _MyComponent(),
    );
  }
}
 
extension ConfigExtensions on BuildContext {
  MyComponentTheme get myCompTheme =>
      findAncestorWidgetOfExactType<MyComponentThemeData>()?.data ?? MyComponentTheme.empty;
}
 
class _MyComponent extends StatelessWidget {
  // properties
 
  const _MyComponent();
 
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(
          'Hello world!',
          style: TextStyle(color: context.myCompTheme.color),
        ),
        Text(
          'World has an error!',
          style: TextStyle(color: context.myCompTheme.errorColor),
        ),
      ],
    );
  }
}