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 totrue
- 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.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
Libraries
- m3_adaptive_theme
- M3 Adaptive Theme