m3_adaptive_theme 1.0.0 copy "m3_adaptive_theme: ^1.0.0" to clipboard
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,
        ),
      ],
    );
  }
}
0
likes
140
points
34
downloads

Publisher

unverified uploader

Weekly Downloads

Effortlessly integrate Material 3 (Material You) adaptive theming into Flutter apps with dynamic color extraction, smart dark/light mode, and platform-aware features.

Repository (GitHub)

Documentation

API reference

License

MIT (license)

Dependencies

cross_file, dynamic_color, file_picker, flutter, get_it, http, shared_preferences

More

Packages that depend on m3_adaptive_theme