Back to Posts

Implementing Dark Mode in Flutter Apps

6 min read

Dark mode is a popular feature in modern apps. This article will teach you how to implement dark mode in Flutter, including theme switching and best practices.

Understanding ThemeData

Flutter's ThemeData class is the foundation for implementing dark mode:

MaterialApp(
  theme: ThemeData(
    brightness: Brightness.light,
    primarySwatch: Colors.blue,
    // Light theme specific styles
  ),
  darkTheme: ThemeData(
    brightness: Brightness.dark,
    primarySwatch: Colors.blue,
    // Dark theme specific styles
  ),
)

Theme Provider Implementation

Create a theme provider to manage theme state:

class ThemeProvider extends ChangeNotifier {
  bool _isDarkMode = false;
  bool get isDarkMode => _isDarkMode;

  void toggleTheme() {
    _isDarkMode = !_isDarkMode;
    notifyListeners();
  }
}

Theme Configuration

Define your light and dark themes:

class AppTheme {
  static ThemeData get lightTheme {
    return ThemeData(
      brightness: Brightness.light,
      primarySwatch: Colors.blue,
      scaffoldBackgroundColor: Colors.white,
      appBarTheme: AppBarTheme(
        backgroundColor: Colors.blue,
        foregroundColor: Colors.white,
      ),
      textTheme: TextTheme(
        bodyText1: TextStyle(color: Colors.black87),
        bodyText2: TextStyle(color: Colors.black87),
      ),
    );
  }

  static ThemeData get darkTheme {
    return ThemeData(
      brightness: Brightness.dark,
      primarySwatch: Colors.blue,
      scaffoldBackgroundColor: Colors.grey[900],
      appBarTheme: AppBarTheme(
        backgroundColor: Colors.grey[800],
        foregroundColor: Colors.white,
      ),
      textTheme: TextTheme(
        bodyText1: TextStyle(color: Colors.white70),
        bodyText2: TextStyle(color: Colors.white70),
      ),
    );
  }
}

Theme Switching Implementation

Implement theme switching in your app:

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Consumer<ThemeProvider>(
      builder: (context, themeProvider, child) {
        return MaterialApp(
          title: 'Flutter Dark Mode Demo',
          theme: AppTheme.lightTheme,
          darkTheme: AppTheme.darkTheme,
          themeMode: themeProvider.isDarkMode ? ThemeMode.dark : ThemeMode.light,
          home: const HomePage(),
        );
      },
    );
  }
}

Theme Switching UI

Add a theme toggle button:

class ThemeToggleButton extends StatelessWidget {
  const ThemeToggleButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Consumer<ThemeProvider>(
      builder: (context, themeProvider, child) {
        return IconButton(
          icon: Icon(
            themeProvider.isDarkMode ? Icons.light_mode : Icons.dark_mode,
          ),
          onPressed: themeProvider.toggleTheme,
        );
      },
    );
  }
}

Persisting Theme Preference

Save theme preference using shared_preferences:

class ThemeProvider extends ChangeNotifier {
  static const String _themeKey = 'isDarkMode';
  bool _isDarkMode = false;
  bool get isDarkMode => _isDarkMode;

  ThemeProvider() {
    _loadTheme();
  }

  Future<void> _loadTheme() async {
    final prefs = await SharedPreferences.getInstance();
    _isDarkMode = prefs.getBool(_themeKey) ?? false;
    notifyListeners();
  }

  Future<void> toggleTheme() async {
    _isDarkMode = !_isDarkMode;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool(_themeKey, _isDarkMode);
    notifyListeners();
  }
}

Custom Theme Extensions

Create custom theme extensions for app-specific colors:

class AppColors extends ThemeExtension<AppColors> {
  final Color success;
  final Color warning;
  final Color error;

  const AppColors({
    required this.success,
    required this.warning,
    required this.error,
  });

  @override
  ThemeExtension<AppColors> copyWith({
    Color? success,
    Color? warning,
    Color? error,
  }) {
    return AppColors(
      success: success ?? this.success,
      warning: warning ?? this.warning,
      error: error ?? this.error,
    );
  }

  @override
  ThemeExtension<AppColors> lerp(ThemeExtension<AppColors>? other, double t) {
    if (other is! AppColors) return this;
    return AppColors(
      success: Color.lerp(success, other.success, t)!,
      warning: Color.lerp(warning, other.warning, t)!,
      error: Color.lerp(error, other.error, t)!,
    );
  }
}

Using Custom Theme Extensions

// In your theme configuration
static ThemeData get lightTheme {
  return ThemeData(
    // ... other theme properties
    extensions: [
      AppColors(
        success: Colors.green,
        warning: Colors.orange,
        error: Colors.red,
      ),
    ],
  );
}

// Usage in widgets
final colors = Theme.of(context).extension<AppColors>()!;
Container(
  color: colors.success,
  child: Text('Success'),
)

Best Practices

  1. Use Semantic Colors: Define colors based on their purpose
  2. Maintain Contrast: Ensure text remains readable in both themes
  3. Test Thoroughly: Verify appearance on different devices
  4. Consider Accessibility: Support system theme preferences
  5. Optimize Performance: Minimize theme-related rebuilds

Testing Theme Implementation

void main() {
  testWidgets('Theme Toggle Test', (WidgetTester tester) async {
    await tester.pumpWidget(
      ChangeNotifierProvider(
        create: (_) => ThemeProvider(),
        child: const MyApp(),
      ),
    );

    // Verify initial theme
    expect(Theme.of(tester.element(find.byType(MyApp))).brightness, 
           equals(Brightness.light));

    // Toggle theme
    await tester.tap(find.byIcon(Icons.dark_mode));
    await tester.pump();

    // Verify theme change
    expect(Theme.of(tester.element(find.byType(MyApp))).brightness, 
           equals(Brightness.dark));
  });
}

Conclusion

Implementing dark mode in Flutter apps involves:

  • Understanding ThemeData and its properties
  • Creating a theme provider for state management
  • Defining light and dark theme configurations
  • Implementing theme switching functionality
  • Persisting theme preferences
  • Using custom theme extensions
  • Following best practices for accessibility and performance

By following these guidelines, you can create a polished dark mode implementation that enhances your app's user experience.

Stay tuned for step-by-step tutorials and examples!