How to Add Dark Mode in Flutter – Step by Step Guide

Flutter Toggle Example

Dark mode has evolved from a trendy feature to an essential part of modern app design. This comprehensive guide will walk you through implementing a robust, persistent dark mode system in Flutter with best practices and advanced features.

Why Dark Mode Matters

User Benefits

  • Reduced Eye Strain: Lower blue light emission and reduced brightness help prevent digital eye fatigue
  • Enhanced Battery Life: OLED displays consume up to 60% less power when displaying dark pixels
  • Improved Accessibility: Better contrast options for users with visual sensitivities
  • Contextual Comfort: Ideal for low-light environments and nighttime usage

Developer Benefits

  • Modern UI Standards: Meets user expectations for contemporary app design
  • System Integration: Seamless integration with device-wide theme preferences
  • Brand Differentiation: Opportunity to create unique visual experiences

Understanding Flutter’s Theme System

Flutter’s theming revolves around three key components:

ThemeData

Defines the visual properties of your app including colors, typography, and component styles.

ThemeMode

Controls which theme is active:

  • ThemeMode.system – Follows device settings (recommended default)
  • ThemeMode.light – Forces light theme
  • ThemeMode.dark – Forces dark theme

Material Design 3

Flutter’s latest Material Design implementation provides enhanced support for dynamic theming and better dark mode defaults.

Complete Implementation

1. Project Setup

First, add the required dependencies to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.2.2
  provider: ^6.0.5

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0

2. Theme Configuration

Create a dedicated theme configuration file (lib/theme/app_theme.dart):

import 'package:flutter/material.dart';

class AppTheme {
  // Brand colors
  static const Color primaryLight = Color(0xFF1976D2);
  static const Color primaryDark = Color(0xFF90CAF9);
  static const Color surfaceLight = Color(0xFFFAFAFA);
  static const Color surfaceDark = Color(0xFF121212);

  // Light theme
  static ThemeData get lightTheme {
    return ThemeData(
      useMaterial3: true,
      brightness: Brightness.light,
      colorScheme: ColorScheme.fromSeed(
        seedColor: primaryLight,
        brightness: Brightness.light,
      ),
      appBarTheme: const AppBarTheme(
        centerTitle: true,
        elevation: 0,
        scrolledUnderElevation: 1,
      ),
      cardTheme: CardTheme(
        elevation: 2,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(12),
        ),
      ),
      elevatedButtonTheme: ElevatedButtonThemeData(
        style: ElevatedButton.styleFrom(
          padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(8),
          ),
        ),
      ),
    );
  }

  // Dark theme
  static ThemeData get darkTheme {
    return ThemeData(
      useMaterial3: true,
      brightness: Brightness.dark,
      colorScheme: ColorScheme.fromSeed(
        seedColor: primaryDark,
        brightness: Brightness.dark,
      ),
      appBarTheme: const AppBarTheme(
        centerTitle: true,
        elevation: 0,
        scrolledUnderElevation: 1,
      ),
      cardTheme: CardTheme(
        elevation: 2,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(12),
        ),
      ),
      elevatedButtonTheme: ElevatedButtonThemeData(
        style: ElevatedButton.styleFrom(
          padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(8),
          ),
        ),
      ),
    );
  }
}

3. Theme Provider (State Management)

Create a theme provider (lib/providers/theme_provider.dart):

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class ThemeProvider with ChangeNotifier {
  static const String _themeKey = 'theme_mode';
  ThemeMode _themeMode = ThemeMode.system;
  
  ThemeMode get themeMode => _themeMode;
  
  bool get isDarkMode {
    if (_themeMode == ThemeMode.system) {
      return WidgetsBinding.instance.platformDispatcher.platformBrightness == 
             Brightness.dark;
    }
    return _themeMode == ThemeMode.dark;
  }

  String get currentThemeName {
    switch (_themeMode) {
      case ThemeMode.light:
        return 'Light';
      case ThemeMode.dark:
        return 'Dark';
      case ThemeMode.system:
        return 'System';
    }
  }

  ThemeProvider() {
    _loadThemeMode();
  }

  /// Load saved theme preference
  Future<void> _loadThemeMode() async {
    final prefs = await SharedPreferences.getInstance();
    final themeIndex = prefs.getInt(_themeKey) ?? 0;
    _themeMode = ThemeMode.values[themeIndex];
    notifyListeners();
  }

  /// Save theme preference
  Future<void> _saveThemeMode() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setInt(_themeKey, _themeMode.index);
  }

  /// Set theme mode
  Future<void> setThemeMode(ThemeMode mode) async {
    if (_themeMode == mode) return;
    
    _themeMode = mode;
    notifyListeners();
    await _saveThemeMode();
  }

  /// Toggle between light and dark (ignores system)
  Future<void> toggleTheme() async {
    if (_themeMode == ThemeMode.light) {
      await setThemeMode(ThemeMode.dark);
    } else {
      await setThemeMode(ThemeMode.light);
    }
  }
}

4. Main Application Setup

Update your lib/main.dart:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:your_app/providers/theme_provider.dart';
import 'package:your_app/theme/app_theme.dart';
import 'package:your_app/screens/home_screen.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => ThemeProvider(),
      child: const MyApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return Consumer<ThemeProvider>(
      builder: (context, themeProvider, child) {
        return MaterialApp(
          title: 'Dark Mode Demo',
          debugShowCheckedModeBanner: false,
          theme: AppTheme.lightTheme,
          darkTheme: AppTheme.darkTheme,
          themeMode: themeProvider.themeMode,
          home: const HomeScreen(),
        );
      },
    );
  }
}

5. Enhanced Home Screen

Create an improved home screen (lib/screens/home_screen.dart):

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:your_app/providers/theme_provider.dart';
import 'package:your_app/widgets/theme_selector_dialog.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Dark Mode Demo'),
        actions: [
          IconButton(
            icon: const Icon(Icons.palette_outlined),
            onPressed: () => _showThemeSelector(context),
            tooltip: 'Change Theme',
          ),
        ],
      ),
      body: Consumer<ThemeProvider>(
        builder: (context, themeProvider, child) {
          return SingleChildScrollView(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                // Theme Status Card
                Card(
                  child: Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Row(
                          children: [
                            Icon(
                              themeProvider.isDarkMode 
                                ? Icons.dark_mode 
                                : Icons.light_mode,
                              color: Theme.of(context).colorScheme.primary,
                            ),
                            const SizedBox(width: 8),
                            Text(
                              'Current Theme',
                              style: Theme.of(context).textTheme.titleMedium,
                            ),
                          ],
                        ),
                        const SizedBox(height: 8),
                        Text(
                          themeProvider.currentThemeName,
                          style: Theme.of(context).textTheme.headlineSmall?.copyWith(
                            color: Theme.of(context).colorScheme.primary,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        if (themeProvider.themeMode == ThemeMode.system)
                          Text(
                            'Following ${themeProvider.isDarkMode ? 'dark' : 'light'} system setting',
                            style: Theme.of(context).textTheme.bodySmall,
                          ),
                      ],
                    ),
                  ),
                ),
                
                const SizedBox(height: 16),
                
                // Quick Toggle
                Card(
                  child: SwitchListTile.adaptive(
                    title: const Text('Dark Mode'),
                    subtitle: const Text('Toggle between light and dark themes'),
                    value: themeProvider.isDarkMode,
                    onChanged: themeProvider.themeMode == ThemeMode.system 
                      ? null 
                      : (value) => themeProvider.toggleTheme(),
                    secondary: Icon(
                      themeProvider.isDarkMode 
                        ? Icons.dark_mode 
                        : Icons.light_mode,
                    ),
                  ),
                ),
                
                const SizedBox(height: 24),
                
                // Sample UI Elements
                Text(
                  'Sample UI Elements',
                  style: Theme.of(context).textTheme.headlineSmall,
                ),
                
                const SizedBox(height: 16),
                
                // Buttons Row
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    ElevatedButton(
                      onPressed: () {},
                      child: const Text('Elevated'),
                    ),
                    FilledButton(
                      onPressed: () {},
                      child: const Text('Filled'),
                    ),
                    OutlinedButton(
                      onPressed: () {},
                      child: const Text('Outlined'),
                    ),
                  ],
                ),
                
                const SizedBox(height: 16),
                
                // Color Palette Preview
                Card(
                  child: Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          'Color Palette',
                          style: Theme.of(context).textTheme.titleMedium,
                        ),
                        const SizedBox(height: 12),
                        _buildColorRow(
                          context, 
                          'Primary', 
                          Theme.of(context).colorScheme.primary,
                        ),
                        _buildColorRow(
                          context, 
                          'Secondary', 
                          Theme.of(context).colorScheme.secondary,
                        ),
                        _buildColorRow(
                          context, 
                          'Surface', 
                          Theme.of(context).colorScheme.surface,
                        ),
                        _buildColorRow(
                          context, 
                          'Background', 
                          Theme.of(context).colorScheme.background,
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showThemeSelector(context),
        tooltip: 'Theme Settings',
        child: const Icon(Icons.palette),
      ),
    );
  }

  Widget _buildColorRow(BuildContext context, String name, Color color) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4.0),
      child: Row(
        children: [
          Container(
            width: 24,
            height: 24,
            decoration: BoxDecoration(
              color: color,
              borderRadius: BorderRadius.circular(4),
              border: Border.all(
                color: Theme.of(context).dividerColor,
                width: 0.5,
              ),
            ),
          ),
          const SizedBox(width: 12),
          Expanded(child: Text(name)),
          Text(
            '#${color.value.toRadixString(16).substring(2).toUpperCase()}',
            style: Theme.of(context).textTheme.bodySmall?.copyWith(
              fontFamily: 'monospace',
            ),
          ),
        ],
      ),
    );
  }

  void _showThemeSelector(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) => const ThemeSelectorDialog(),
    );
  }
}

6. Theme Selector Dialog

Create a theme selector widget (lib/widgets/theme_selector_dialog.dart):

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:your_app/providers/theme_provider.dart';

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

  @override
  Widget build(BuildContext context) {
    return Consumer<ThemeProvider>(
      builder: (context, themeProvider, child) {
        return AlertDialog(
          title: const Text('Choose Theme'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              _buildThemeOption(
                context,
                'System Default',
                'Follow device settings',
                Icons.settings_suggest,
                ThemeMode.system,
                themeProvider.themeMode,
                themeProvider.setThemeMode,
              ),
              _buildThemeOption(
                context,
                'Light Mode',
                'Always use light theme',
                Icons.light_mode,
                ThemeMode.light,
                themeProvider.themeMode,
                themeProvider.setThemeMode,
              ),
              _buildThemeOption(
                context,
                'Dark Mode',
                'Always use dark theme',
                Icons.dark_mode,
                ThemeMode.dark,
                themeProvider.themeMode,
                themeProvider.setThemeMode,
              ),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: const Text('Close'),
            ),
          ],
        );
      },
    );
  }

  Widget _buildThemeOption(
    BuildContext context,
    String title,
    String subtitle,
    IconData icon,
    ThemeMode mode,
    ThemeMode currentMode,
    Function(ThemeMode) onChanged,
  ) {
    final isSelected = mode == currentMode;
    
    return RadioListTile<ThemeMode>(
      title: Text(title),
      subtitle: Text(subtitle),
      secondary: Icon(
        icon,
        color: isSelected 
          ? Theme.of(context).colorScheme.primary 
          : null,
      ),
      value: mode,
      groupValue: currentMode,
      onChanged: (value) {
        if (value != null) {
          onChanged(value);
          Navigator.of(context).pop();
        }
      },
    );
  }
}

Advanced Features

System Theme Detection

The implementation automatically detects and responds to system-wide theme changes when ThemeMode.system is selected.

Persistent Preferences

Theme preferences are saved locally and restored when the app restarts, providing a seamless user experience.

Smooth Transitions

Flutter automatically handles smooth transitions between themes, including animated color changes.

Accessibility Considerations

  • High contrast ratios for better readability
  • Proper semantic labeling for screen readers
  • Adaptive controls that work well in both themes

Best Practices

  1. Use Material Design 3: Leverage useMaterial3: true for better theming defaults
  2. Test Both Themes: Ensure all UI elements look good in both light and dark modes
  3. Consider Brand Colors: Adapt your brand colors appropriately for each theme
  4. Performance: Use Consumer widgets to minimize rebuilds
  5. User Choice: Always provide easy access to theme switching options

Customization Ideas

  • Multiple Color Schemes: Add support for different color palettes
  • Scheduled Themes: Automatically switch themes based on time of day
  • Custom Themes: Allow users to create their own color combinations
  • Theme Animations: Add custom transition animations between themes

Conclusion

This comprehensive implementation provides a solid foundation for dark mode in your Flutter applications. The modular approach makes it easy to extend and customize while following Flutter best practices for state management and theming.

The combination of Provider for state management, SharedPreferences for persistence, and Material Design 3 for modern theming creates a robust and user-friendly dark mode system that your users will appreciate.

Remember to test your implementation across different devices and screen sizes to ensure a consistent experience for all users.

Leave a Reply

Your email address will not be published. Required fields are marked *