M3 Adaptive Theme

Effortlessly integrate Material 3 (Material You) adaptive theming into Flutter apps.

Features

🎨 Dynamic Color Extraction

  • Automatically adopt device wallpaper colors on Android 12+
  • Fallback to harmonized color schemes on other platforms
  • Support for custom seed colors

πŸŒ“ Smart Dark/Light Mode

  • Automatic system theme detection
  • WCAG-compliant color schemes
  • Smooth theme transitions

πŸ“± Platform-Aware

  • Full Material You support on Android 12+
  • Graceful fallbacks for iOS and web
  • Consistent experience across platforms

⚑ Zero Boilerplate

  • Simple app wrapper setup
  • Real-time theme adaptation
  • Minimal configuration required

πŸŽ›οΈ Advanced Customization

  • Fine-grained elevation control
  • Motion and animation settings
  • Typography customization
  • Accessibility features
  • Theme preset management

Installation

Add this to your pubspec.yaml:

dependencies:
  m3_adaptive_theme: ^1.0.0

Then run:

flutter pub get

Quick Start

Wrap your app with M3AdaptiveTheme:

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return M3AdaptiveTheme(
      initialConfig: const ThemeConfig(
        themeMode: ThemeMode.system,
        seedColor: Colors.blue,
        useDynamicColors: true,
      ),
      useTransition: true,
      transitionDuration: const Duration(milliseconds: 500),
      transitionBuilder: (context, child) => ScaleFadeThemeTransition(
        child: child,
      ),
      child: const MyHomePage(),
    );
  }
}

That's it! Your app now follows Material 3 design, adapts to the device's wallpaper colors (on supported platforms), and has smooth theme transitions.

Theme Customization

Access the theme manager anywhere in your app:

final themeManager = M3AdaptiveTheme.of(context);

// Change theme mode
themeManager.setThemeMode(ThemeMode.dark);

// Change seed color
themeManager.setSeedColor(Colors.purple);

// Toggle dynamic colors
themeManager.toggleDynamicColors(true);

Built-in Widgets

The package includes several ready-to-use widgets:

Theme Mode Toggle

ThemeModeToggle(themeManager: themeManager)

Seed Color Picker

SeedColorPicker(themeManager: themeManager)

Dynamic Color Switch

DynamicColorSwitch(themeManager: themeManager)

Theme Preset Grid

ThemePresetGrid(
  themeManager: themeManager,
  onPresetSelected: (preset) {
    print('Selected preset: ${preset.name}');
  },
)

Theme Transition Widgets

Enhance the user experience with smooth theme transitions:

Animated Background Transition

AnimatedThemeTransition(
  duration: const Duration(milliseconds: 500),
  curve: Curves.easeInOut,
  child: YourWidget(),
)

Cross-Fade Theme Transition

CrossFadeThemeTransition(
  duration: const Duration(milliseconds: 500),
  child: YourWidget(),
)

Scale-Fade Theme Transition

ScaleFadeThemeTransition(
  duration: const Duration(milliseconds: 500),
  child: YourWidget(),
)

Save Theme Preset Dialog

final preset = await SaveThemePresetDialog.show(
  context,
  themeManager: themeManager,
  initialName: 'My Theme',
);

if (preset != null) {
  print('Saved preset: ${preset.name}');
}

Theme Persistence

Theme presets are automatically saved to local storage. You can manage them with:

// Save current theme as a preset
final preset = await themeManager.savePreset('My Theme');

// Get all saved presets
final presets = await themeManager.getPresets();

// Apply a preset
await themeManager.applyPreset(preset);

// Delete a preset
await themeManager.deletePreset(preset.id);

Accessibility Utilities

Ensure your app's text remains readable regardless of background color:

// Check if text has good contrast with background
bool isAccessible = AccessibilityUtils.hasGoodContrast(
  textColor,
  backgroundColor,
  isLargeText: false, // Set to true for text >= 18pt
);

// Improve text contrast while preserving color characteristics
Color accessibleColor = AccessibilityUtils.improveContrast(
  originalTextColor,
  backgroundColor,
);

// Quickly get an accessible text color (black or white) based on background
Color textColor = AccessibilityUtils.getAccessibleTextColor(backgroundColor);

// Apply emphasis levels to maintain text hierarchy
Color highEmphasisText = AccessibilityUtils.applyEmphasis(
  baseTextColor,
  EmphasisLevel.high,
);

Custom Builder

For more control over your app's structure, use the builder parameter:

M3AdaptiveTheme(
  initialConfig: config,
  builder: (context, lightTheme, darkTheme, themeMode, child) {
    return MaterialApp(
      theme: lightTheme,
      darkTheme: darkTheme,
      themeMode: themeMode,
      home: child,
      navigatorKey: navigatorKey,
      routes: routes,
      // Other MaterialApp properties
    );
  },
  child: const MyHomePage(),
)

License

This project is licensed under the MIT License - see the LICENSE file for details.

Advanced Usage

Custom Theme Extensions

You can extend the theme with your own custom theme extensions:

// Define your custom theme extension
class MyCustomTheme extends ThemeExtension<MyCustomTheme> {
  final Color customColor;
  final BorderRadius borderRadius;

  MyCustomTheme({
    required this.customColor,
    this.borderRadius = const BorderRadius.all(Radius.circular(8)),
  });

  @override
  ThemeExtension<MyCustomTheme> copyWith({
    Color? customColor,
    BorderRadius? borderRadius,
  }) {
    return MyCustomTheme(
      customColor: customColor ?? this.customColor,
      borderRadius: borderRadius ?? this.borderRadius,
    );
  }

  @override
  ThemeExtension<MyCustomTheme> lerp(ThemeExtension<MyCustomTheme>? other, double t) {
    if (other is! MyCustomTheme) {
      return this;
    }
    return MyCustomTheme(
      customColor: Color.lerp(customColor, other.customColor, t) ?? customColor,
      borderRadius: BorderRadius.lerp(borderRadius, other.borderRadius, t) ?? borderRadius,
    );
  }
}

// Add it to your theme in a builder
M3AdaptiveTheme(
  initialConfig: config,
  builder: (context, lightTheme, darkTheme, themeMode, child) {
    // Add your custom theme extension
    final lightThemeWithExtension = lightTheme.copyWith(
      extensions: <ThemeExtension<dynamic>>[
        MyCustomTheme(
          customColor: Colors.blue.shade300,
        ),
      ],
    );

    final darkThemeWithExtension = darkTheme.copyWith(
      extensions: <ThemeExtension<dynamic>>[
        MyCustomTheme(
          customColor: Colors.blue.shade700,
        ),
      ],
    );

    return MaterialApp(
      theme: lightThemeWithExtension,
      darkTheme: darkThemeWithExtension,
      themeMode: themeMode,
      home: child,
    );
  },
  child: const MyHomePage(),
)

// Use it in your widgets
final myCustomTheme = Theme.of(context).extension<MyCustomTheme>()!;
Container(
  decoration: BoxDecoration(
    color: myCustomTheme.customColor,
    borderRadius: myCustomTheme.borderRadius,
  ),
  child: Text('Custom themed container'),
)

Theme Animation Customization

Customize theme animations by combining transition widgets:

class CustomThemeTransition extends StatelessWidget {
  final Widget child;
  final Duration duration;

  const CustomThemeTransition({
    super.key,
    required this.child,
    this.duration = const Duration(milliseconds: 500),
  });

  @override
  Widget build(BuildContext context) {
    return AnimatedThemeTransition(
      duration: duration,
      child: ScaleFadeThemeTransition(
        duration: duration,
        child: child,
      ),
    );
  }
}

// Use in M3AdaptiveTheme
M3AdaptiveTheme(
  initialConfig: config,
  useTransition: true,
  transitionBuilder: (context, child) => CustomThemeTransition(child: child),
  child: const MyApp(),
)

Platform-Specific Theming

Apply different theme configurations based on platform:

M3AdaptiveTheme(
  initialConfig: ThemeConfig(
    themeMode: ThemeMode.system,
    seedColor: Platform.isIOS ? Colors.blue : Colors.teal,
    useDynamicColors: Platform.isAndroid, // Only use dynamic colors on Android
  ),
  child: MyApp(),
)

Complete API Reference

ThemeConfig

Property Type Description
themeMode ThemeMode Light, dark, or system theme mode
seedColor Color Base color used to generate the color scheme
useDynamicColors bool Whether to use dynamic colors from wallpaper on supported platforms
darkSeedColor Color? Optional separate seed color for dark theme
brightness Brightness? Override system brightness detection

ThemeManager

Method Description
setThemeMode(ThemeMode mode) Change between light, dark, or system theme
setSeedColor(Color color) Change the seed color used for generating the color scheme
toggleDynamicColors(bool value) Enable or disable dynamic colors from wallpaper
savePreset(String name) Save current theme as a preset
getPresets() Get all saved theme presets
applyPreset(ThemePreset preset) Apply a saved theme preset
deletePreset(String id) Delete a saved theme preset
getCurrentTheme() Get current theme configuration

M3AdaptiveTheme

Property Type Description
initialConfig ThemeConfig Initial theme configuration
builder Widget Function(...) Custom builder for MaterialApp
useTransition bool Whether to use theme transitions
transitionDuration Duration Duration for theme transitions
transitionBuilder Widget Function(...) Custom transition builder
child Widget Child widget (your app content)

ThemePreset

Property Type Description
id String Unique identifier for the preset
name String Display name for the preset
seedColor Color Seed color for the preset
isDark bool Whether the preset is for dark theme
useDynamicColors bool Whether the preset uses dynamic colors
createdAt DateTime When the preset was created
lastUsed DateTime When the preset was last applied

Troubleshooting

Dynamic Colors Not Working

If dynamic colors aren't working:

  • Ensure you're running on Android 12+ or using a supported platform
  • Check that useDynamicColors is set to true
  • Some Android ROM manufacturers disable dynamic colors feature
// Force disable dynamic colors for testing
M3AdaptiveTheme(
  initialConfig: ThemeConfig(
    useDynamicColors: false,
    seedColor: Colors.blue,
  ),
  child: MyApp(),
)

Theme Changes Not Persisting

If theme changes aren't saved between app restarts:

  • Verify the PresetRepository is initialized correctly
  • Check for storage permission issues on the device
  • Ensure the app has write permissions

Performance Issues with Transitions

If transitions are causing performance issues:

  • Use simpler transitions for low-end devices
  • Reduce transition duration
  • Disable transitions for specific widgets
// Simpler, more performant transition
M3AdaptiveTheme(
  useTransition: true,
  transitionDuration: const Duration(milliseconds: 300),
  transitionBuilder: (context, child) =>
    AnimatedThemeTransition(child: child),
  child: MyApp(),
)

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Libraries

m3_adaptive_theme
M3 Adaptive Theme