m3_adaptive_theme 1.0.0
m3_adaptive_theme: ^1.0.0 copied to clipboard
Effortlessly integrate Material 3 (Material You) adaptive theming into Flutter apps with dynamic color extraction, smart dark/light mode, and platform-aware features.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:m3_adaptive_theme/m3_adaptive_theme.dart';
import 'screens/landing_page.dart';
import 'screens/theme_customizer.dart';
import 'screens/theme_inspector_screen.dart';
import 'widgets/theme_showcase.dart';
void main() {
runApp(const ExampleApp());
}
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return const M3AdaptiveTheme(
initialConfig: ThemeConfig(
themeMode: ThemeMode.system,
seedColor: Colors.blue,
useDynamicColors: true,
),
useTransition: true,
child: AppNavigator(),
);
}
}
class AppNavigator extends StatefulWidget {
const AppNavigator({super.key});
@override
State<AppNavigator> createState() => _AppNavigatorState();
}
class _AppNavigatorState extends State<AppNavigator> {
int _currentIndex = 0;
final _pages = [
(_) => LandingPage(
onShowcasePressed: () => _navigateToPage(1),
onCustomizerPressed: () => _navigateToPage(2),
),
(_) => const ThemeShowcaseScreen(),
(_) => const ThemeCustomizer(),
(_) => const ThemeInspectorScreen(),
];
static void _navigateToPage(int index) {
// This gets handled by our app navigator
}
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: IndexedStack(
index: _currentIndex,
children: [
LandingPage(
onShowcasePressed: () => setState(() => _currentIndex = 1),
onCustomizerPressed: () => setState(() => _currentIndex = 2),
),
const ThemeShowcaseScreen(),
const ThemeCustomizer(),
const ThemeInspectorScreen(),
],
),
bottomNavigationBar: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (index) {
setState(() => _currentIndex = index);
},
destinations: const [
NavigationDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home),
label: 'Home',
),
NavigationDestination(
icon: Icon(Icons.style_outlined),
selectedIcon: Icon(Icons.style),
label: 'Showcase',
),
NavigationDestination(
icon: Icon(Icons.tune_outlined),
selectedIcon: Icon(Icons.tune),
label: 'Customize',
),
NavigationDestination(
icon: Icon(Icons.bug_report_outlined),
selectedIcon: Icon(Icons.bug_report),
label: 'Inspector',
),
],
),
),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
final themeManager = M3AdaptiveTheme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('M3 Adaptive Theme Demo'),
actions: [
ThemeModeToggle(themeManager: themeManager),
IconButton(
icon: const Icon(Icons.save),
tooltip: 'Save current theme',
onPressed: () => _saveThemePreset(context, themeManager),
),
],
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Seed color picker
SeedColorPicker(themeManager: themeManager),
const Divider(height: 32),
// Dynamic colors switch
DynamicColorSwitch(themeManager: themeManager),
const Divider(height: 32),
// Display theme presets
_buildPresetSection(context, themeManager),
const Divider(height: 32),
// Theme transition demo
_buildThemeTransitionDemo(context),
const Divider(height: 32),
// Accessibility demo
_buildAccessibilityDemo(context),
const Divider(height: 32),
// UI component showcase
const Text(
'Theme Showcase',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
const ThemeShowcase(title: 'Buttons'),
const SizedBox(height: 20),
_buildColorsShowcase(context),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
);
}
Widget _buildPresetSection(BuildContext context, ThemeManager themeManager) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Saved Presets',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
TextButton.icon(
icon: const Icon(Icons.add),
label: const Text('Add New'),
onPressed: () => _saveThemePreset(context, themeManager),
),
],
),
const SizedBox(height: 8),
SizedBox(
height: 200,
child: ThemePresetGrid(
themeManager: themeManager,
crossAxisCount: 2,
onPresetDeleted: (preset) =>
_deletePreset(context, themeManager, preset),
),
),
],
);
}
Widget _buildThemeTransitionDemo(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final colorScheme = Theme.of(context).colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Theme Transitions',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// Description
Text(
'The package includes several built-in theme transition effects. '
'Try toggling the theme mode to see them in action.',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
// Sample cards with different transition effects
Row(
children: [
Expanded(
child: AnimatedThemeTransition(
duration: const Duration(milliseconds: 500),
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Icon(
isDark ? Icons.nightlight_round : Icons.wb_sunny,
size: 32,
color: colorScheme.primary,
),
const SizedBox(height: 8),
Text(
'Background\nTransition',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleSmall,
),
],
),
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: CrossFadeThemeTransition(
duration: const Duration(milliseconds: 500),
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Icon(
isDark ? Icons.nightlight_round : Icons.wb_sunny,
size: 32,
color: colorScheme.primary,
),
const SizedBox(height: 8),
Text(
'Cross-Fade\nTransition',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleSmall,
),
],
),
),
),
),
),
],
),
],
);
}
Widget _buildAccessibilityDemo(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
// Sample background color - not accessible enough for text
final backgroundColor = colorScheme.primary;
final originalTextColor = colorScheme.onSurface;
// Improved text color for accessibility
final accessibleTextColor = AccessibilityUtils.improveContrast(
originalTextColor,
backgroundColor,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Accessibility Features',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Text(
'The package includes utilities to ensure text remains accessible '
'regardless of background color.',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Text(
'Standard Text',
style: Theme.of(context).textTheme.titleMedium!.copyWith(
color: originalTextColor,
),
),
const SizedBox(height: 8),
Text(
'This text might be difficult to read on this background',
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: originalTextColor,
),
),
],
),
),
),
const SizedBox(width: 16),
Expanded(
child: Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Text(
'Accessible Text',
style: Theme.of(context).textTheme.titleMedium!.copyWith(
color: accessibleTextColor,
),
),
const SizedBox(height: 8),
Text(
'This text is adjusted for better contrast (WCAG compliant)',
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: accessibleTextColor,
),
),
],
),
),
),
],
),
const SizedBox(height: 8),
Text(
'Contrast: ${AccessibilityUtils.hasGoodContrast(accessibleTextColor, backgroundColor) ? "Good" : "Poor"}',
style: Theme.of(context).textTheme.bodySmall,
),
],
);
}
Widget _buildColorsShowcase(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Colors',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
Wrap(
spacing: 10,
runSpacing: 10,
alignment: WrapAlignment.center,
children: [
_ColorSample(
color: Theme.of(context).colorScheme.primary,
name: 'Primary',
),
_ColorSample(
color: Theme.of(context).colorScheme.secondary,
name: 'Secondary',
),
_ColorSample(
color: Theme.of(context).colorScheme.tertiary,
name: 'Tertiary',
),
_ColorSample(
color: Theme.of(context).colorScheme.error,
name: 'Error',
),
],
),
],
);
}
Future<void> _saveThemePreset(
BuildContext context, ThemeManager themeManager) async {
final preset = await SaveThemePresetDialog.show(
context,
themeManager: themeManager,
initialName: 'My Theme',
);
if (preset != null && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Saved preset: ${preset.name}')),
);
}
}
Future<void> _deletePreset(
BuildContext context,
ThemeManager themeManager,
ThemePreset preset,
) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete Preset'),
content: Text('Are you sure you want to delete "${preset.name}"?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('CANCEL'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('DELETE'),
),
],
),
);
if (confirmed == true) {
await themeManager.deletePreset(preset.id);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Deleted preset: ${preset.name}')),
);
}
}
}
}
class ThemeShowcase extends StatelessWidget {
final String title;
final Widget? child;
const ThemeShowcase({
super.key,
required this.title,
this.child,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
child ?? _DefaultButtonsShowcase(),
],
);
}
}
class _DefaultButtonsShowcase extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ElevatedButton(
onPressed: () {},
child: const Text('Elevated Button'),
),
const SizedBox(height: 8),
FilledButton(
onPressed: () {},
child: const Text('Filled Button'),
),
const SizedBox(height: 8),
OutlinedButton(
onPressed: () {},
child: const Text('Outlined Button'),
),
const SizedBox(height: 8),
TextButton(
onPressed: () {},
child: const Text('Text Button'),
),
],
);
}
}
class _ColorSample extends StatelessWidget {
final Color color;
final String name;
const _ColorSample({
required this.color,
required this.name,
});
@override
Widget build(BuildContext context) {
final textColor =
color.computeLuminance() > 0.5 ? Colors.black : Colors.white;
return Column(
children: [
Container(
width: 70,
height: 70,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Center(
child: Text(
name.characters.first,
style: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
fontSize: 24,
),
),
),
),
const SizedBox(height: 4),
Text(
name,
style: Theme.of(context).textTheme.bodySmall,
),
],
);
}
}