Skip to content

Theme Extensions

Kenny Mochizuki Escalona edited this page Aug 14, 2026 · 1 revision

Theme Extensions

Theme extensions allow components to store custom theme data without polluting the core LayrzThemeData class. This page explains the extension mechanism, why it exists, and how to implement and use custom extensions in your components.


Overview

The LayrzThemeExtension<T> mechanism provides a sanctioned way for M2+ components to add their own theme-scoped data. Instead of adding fields directly to LayrzThemeData, a component can define an extension class that holds its tokens and state, register it with the theme, and retrieve it at build time.

Why Extensions?

Without extensions, there are two bad choices:

  1. Add everything to LayrzThemeData — bloats the core theme with hundreds of component-specific fields. The core theme becomes cluttered and unmaintainable.
  2. Let components hardcode style values — breaks theming consistency and prevents runtime theme changes from affecting components.

Extensions solve this by providing a registry where components can store their own data without modifying LayrzThemeData.


The LayrzThemeExtension Contract

All custom extensions must implement LayrzThemeExtension<T>:

abstract class LayrzThemeExtension<T extends LayrzThemeExtension<T>> {
  /// Return the type of this extension (used as the map key).
  Object get type => T;
  
  /// Return a copy with some fields changed.
  T copyWith({...});
  
  /// Linearly interpolate between two extensions.
  T lerp(covariant LayrzThemeExtension<T>? other, double t);
}

Key Points

  • Self-bounded generic T extends LayrzThemeExtension<T>: This ensures the type is sound and prevents accidental type mismatches at lookup time.
  • type getter returns T: Used internally as the map key. You do not need to override this; the SDK handles it.
  • copyWith(): Returns a copy with specified fields changed. Used for theme customization and immutability.
  • lerp(): Interpolates between two extensions. Used for smooth theme animations.

Example: Creating a Button Extension

Here's a complete example of a custom button extension:

// lib/buttons/src/button_extension.dart

@immutable
class LayrzButtonExtension extends LayrzThemeExtension<LayrzButtonExtension> {
  /// Border radius for all buttons in this theme.
  final double borderRadius;
  
  /// Minimum height for buttons.
  final double minHeight;
  
  /// Padding for button content.
  final EdgeInsets padding;
  
  const LayrzButtonExtension({
    this.borderRadius = 8.0,
    this.minHeight = 48.0,
    this.padding = const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
  });
  
  @override
  LayrzButtonExtension copyWith({
    double? borderRadius,
    double? minHeight,
    EdgeInsets? padding,
  }) {
    return LayrzButtonExtension(
      borderRadius: borderRadius ?? this.borderRadius,
      minHeight: minHeight ?? this.minHeight,
      padding: padding ?? this.padding,
    );
  }
  
  @override
  LayrzButtonExtension lerp(
    covariant LayrzThemeExtension<LayrzButtonExtension>? other,
    double t,
  ) {
    if (other is! LayrzButtonExtension) return this;
    
    return LayrzButtonExtension(
      borderRadius: lerpDouble(borderRadius, other.borderRadius, t) ?? borderRadius,
      minHeight: lerpDouble(minHeight, other.minHeight, t) ?? minHeight,
      padding: EdgeInsets.lerp(padding, other.padding, t) ?? padding,
    );
  }
  
  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is LayrzButtonExtension &&
          runtimeType == other.runtimeType &&
          borderRadius == other.borderRadius &&
          minHeight == other.minHeight &&
          padding == other.padding;
  
  @override
  int get hashCode => Object.hash(borderRadius, minHeight, padding);
}

Registering Extensions on a Theme

When creating a LayrzThemeData, pass a list of extensions:

final theme = LayrzThemeData.light(
  extensions: [
    LayrzButtonExtension(borderRadius: 12.0),
    LayrzInputExtension(borderColor: Colors.grey),
  ],
);

Alternatively, add extensions to an existing theme via copyWith():

final customTheme = theme.copyWith(
  extensions: [
    ...theme.extensions.values,
    LayrzButtonExtension(borderRadius: 12.0),
  ],
);

Retrieving Extensions in Components

Use the extension<T>() or maybeExtension<T>() methods on LayrzThemeData (or via the convenience getter on BuildContext):

class LayrzButton extends StatelessWidget {
  const LayrzButton({required this.child});
  
  final Widget child;
  
  @override
  Widget build(BuildContext context) {
    // Option 1: Use maybeExtension (returns null if not found)
    final buttonExtension = context.theme.maybeExtension<LayrzButtonExtension>();
    final borderRadius = buttonExtension?.borderRadius ?? 8.0;
    
    // Option 2: Use extension (asserts if not found)
    final buttonExtension2 = context.theme.extension<LayrzButtonExtension>();
    
    return Container(
      padding: buttonExtension?.padding,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(borderRadius),
      ),
      child: child,
    );
  }
}

Naming Convention

  • extension<T>() — Use when the extension is required. Asserts if not found. Name mirrors LayrzTheme.of(context).
  • maybeExtension<T>() — Use when the extension is optional. Returns null if not found. Name mirrors LayrzTheme.maybeOf(context).

Automatic Propagation Across Boundaries

Because extensions are stored on LayrzThemeData and LayrzTheme extends InheritedTheme, extensions automatically flow through Overlay and route boundaries — no special handling needed.

// Inside a dialog, the button extension is still accessible:
showDialog(
  context: context,
  builder: (dialogContext) {
    final buttonExtension = dialogContext.theme.maybeExtension<LayrzButtonExtension>();
    // This works because LayrzTheme.wrap() passes the entire LayrzThemeData
    return LayrzButton(...);
  },
);

Best Practices

  1. One extension per component category — Define LayrzButtonExtension, LayrzInputExtension, etc., not one giant extension for all buttons.
  2. Make extensions immutable — Use @immutable, implement == and hashCode, and provide copyWith().
  3. Use maybeExtension<T>() for optional customization — Components should work without their extension, using sensible defaults.
  4. Implement lerp() for smooth animations — When themes change via setState(), extensions interpolate smoothly.
  5. Keep extensions focused — Store only theme-scoped data, not component state. State lives in WidgetState or the widget itself.

Migration from layrz_theme

If migrating from layrz_theme, any custom theme fields you added to ThemedThemeData should become extensions in layrz_ui:

// Old (layrz_theme)
class CustomThemedThemeData extends ThemedThemeData {
  final double buttonBorderRadius = 8.0;
}

// New (layrz_ui)
class LayrzButtonExtension extends LayrzThemeExtension<LayrzButtonExtension> {
  final double borderRadius = 8.0;
  // ...
}

Related Architecture

See the Theming page for details on how extensions fit into the broader theme system.

For more on theme structure and layering, see architecture.md.


Last updated: 2026-08-14
Related pages: Theming, Design-Tokens, LayrzApp

Clone this wiki locally